[Optimize This]: Langsame LINQ to Objects-Abfrage

8

Ich habe diese Frage, die mich stört; es ist als neuer Abfrageoperator gekapselt, ich habe zwei Versionen davon gemacht, um zu sehen, welche besser ist. Beide machen schrecklich.

Erster Versuch; deklarativer Stil

%Vor%

Zweiter Versuch: Imperativ "Rendite" Stil

%Vor%

Tatsächlich ist der zweite Versuch schlechter, sowohl was die Lesbarkeit, die Kompositionalität als auch die Geschwindigkeit betrifft.

Irgendwelche Hinweise, wie Sie das optimieren können?

    
Bent Rasmussen 08.02.2010, 14:41
quelle

8 Antworten

9

Ich vermute, dass das Problem, das Sie haben, mit der Tatsache zusammenhängt, dass das Endergebnis mindestens eine O (n ^ 2) -Operation ist, möglicherweise schlechter; Ich habe noch nicht alles in meinem Kopf gearbeitet.

Warum ist das? Nehmen wir an, Sie haben [1, 2, 3, 4, 5, 6] und Sie teilen das auf, was Sie für {{1, 2}, {3, 4}, {5, 6}}

Das haben Sie nicht gemacht. Du hast das tatsächlich aufgeteilt in {nimm die ersten zwei, nimm die ersten zwei und wirf sie ab und nimm die nächsten zwei, nimm die ersten zwei und wirf dann ab und nimm die nächsten zwei und wirf sie ab und nimm den dritten zwei}

Beachten Sie, wie jeder Schritt auf dem Weg das Ergebnis neu berechnet? Das liegt daran, dass das Array zwischen Aufrufen der Enumeration wechseln könnte. LINQ wurde entwickelt, um Ihnen immer aktuelle Ergebnisse zu liefern. Sie schreiben eine Abfrage, die bedeutet "Überspringe die ersten vier und wiederhole die nächsten zwei", genau das bekommst du - eine Abfrage, die diesen Code ausführt, wenn du ihn aufzählst.

Ist die Originalsequenz klein genug und schnell genug, dass Sie das Ganze in den Speicher einlesen und alles auf einmal aufteilen können, anstatt es so träge zu tun? Ist die Sequenz alternativ indizierbar? Wenn alles, was Sie bekommen, Vorwärtszugriff auf die Sequenz ist und es zu groß oder zu langsam ist, um in den Speicher auf einmal zu lesen, dann gibt es nicht viel, was Sie hier tun können. Aber wenn Sie eine oder beide dieser Eigenschaften haben, können Sie dies zumindest linear machen.

    
Eric Lippert 08.02.2010, 15:22
quelle
10

Wenn ich Ihre Frage richtig verstanden habe, versuchen Sie eine faule Implementierung eines Enumerators zu erstellen, der eine größere Sammlung von Elementen in kleinere aufzählbare Objektgruppen aufteilt.

Zum Beispiel könnte eine Folge von einer Million Zahlen in "Abschnitte" aufgeteilt werden, von denen jeder nur 100 von ihnen erzeugt, und Sie möchten, dass alles lazy gemacht wird, dh. keine 100 Gegenstände in einer Liste sammeln, bevor sie produziert werden.

Zuerst werden Ihre Versuche mehrmals über die Sammlung wiederholt, was schlecht ist, daher das Leistungsproblem.

Wenn Sie versuchen, eine reine faule Implementierung zu erstellen, sollten Sie die folgenden Probleme berücksichtigen:

  • Sie möchten nur einmal über die zugrunde liegende Sammlung iterieren
  • Sie sollten Enumerables zurückgeben, die den zugrunde liegenden Enumerator wiederverwenden
  • Sie müssen damit umgehen, dass ein Abschnitt, den Sie zurückgeben, nicht vollständig aufgezählt ist (zum Beispiel brauchte der aufrufende Code nur die ersten 50 dieser 100 Elemente).

Bearbeiten : Bevor ich auf meine einfache Lösung eingehe, hier ein paar Vorbehalte:

  • Sie können nicht jeden Abschnitt für später speichern, dh. Sie können nicht: collection.Sequence(10).ToArray() , um ein Array von Abschnitten zu erhalten.
  • Sie können nicht für jeden Abschnitt mehr als einmal aufzählen, da dabei eine verborgene zugrunde liegende Datenstruktur geändert wird.

Grundsätzlich: Meine Lösung ist kein allgemeiner Zweck . Sie sollten den @LBushkin Kommentar zu MoreLinq Batch, wenn Sie dies benötigen, und ich würde zögern, meinen Code in eine Klassenbibliothek zu legen, es müsste lokal sein, wo es gebraucht wird oder in etwas umbenannt werden, das Sie deutlich vor den damit verbundenen Problemen warnt.

Hier ist eine simple Implementierung, ich bin mir ziemlich sicher, dass es hier Fehler gibt, also sollten Sie vielleicht eine Menge Unit-Testing für Edgecases implementieren:

%Vor%

Ausgabe:

%Vor%

Dinge, die Sie testen sollten:

  • Die leere Eingabesammlung erzeugt keine Abschnitte
  • Eine Sammlung mit genau der richtigen Anzahl von Elementen erzeugt nur einen Abschnitt
  • Eine Sammlung, die ein Multiplum der Elemente in Sektionsgröße enthält (dh 10, 20, 30, usw. Anzahl der Elemente mit einer Sektionsgröße von 5 oder 10), erzeugt nach all den keinen leeren Abschnitt erwartete
  • Dass es tatsächlich faul ist, wenn Sie über den ersten 10-Element-Abschnitt aufzählen, aber nur die ersten 5 des zweiten Abschnitts, werden nur die ersten 15 Elemente der zugrunde liegenden Sammlung über
  • aufgelistet
quelle
4

Wo immer es möglich ist, versuche ich nur einmal innerhalb eines Operators durch eine Quelle zu iterieren. Wenn die Quelle etwas wie das Ergebnis eines Reverse() -Operators ist, könnte der Aufruf von Any , Take und Skip eine Menge unangenehmer Leistung verursachen.

Es ist nicht ganz klar, was Ihr Operator zu tun versucht, aber wenn Sie es tun können, ohne die Quelle mehrmals zu lesen, kann das gut helfen - obwohl es sehr davon abhängen wird, was die Eingabe ist.

    
Jon Skeet 08.02.2010 14:51
quelle
3

Hier ist ein anderer Ansatz, der linq nicht verwendet, der viel schneller war als Ihre zweite Methode:

%Vor%     
JoshBerke 08.02.2010 15:16
quelle
1

Ist das schneller? Es sollte sein, weil es nur eine einzige Iteration durch die Quellsequenz benötigt.

%Vor%     
LukeH 08.02.2010 15:06
quelle
0

Ich hatte heute eine Idee; Sieh dir das an

%Vor%

Dies ist immer noch nicht super-elegant, aber zumindest die Implementierung ist sehr kurz und lesbar.

Es kann sehr interessant sein, Abfrageoperatoren über IEnumerator zu untersuchen.

Und aus praktischen Gründen

%Vor%     
Bent Rasmussen 19.02.2010 09:41
quelle
0

Wie wäre es mit einer Erweiterungsmethode?

%Vor%

einige Tests:

%Vor%     
Handcraftsman 03.03.2010 20:02
quelle
0

Müssen Sie Ihre ursprüngliche Quelle aus irgendeinem Grund behalten, während Sie fortschreiten? Wenn nicht, warum verwenden Sie keine Rekursion und verwenden Sie hd :: tl style, um den Kopf zu ziehen, übergeben Sie den tl in den rekursiven Aufruf, und auf jeder geraden Zahl Rekursion verschmelzen Sie die beiden Sie in einem Abschnitt sitzen?

Mit der aktualisierten Version der experimentellen Ix-Erweiterungen könnten Sie die Operatoren Window oder Buffer verwenden, um ein sliding window , das sollte erreichen, was Sie suchen.

    
user29439 19.02.2010 16:49
quelle