2012-04-25 12 views
7

Angenommen, ich den folgenden Code haben:Cachiert LINQ berechnete Werte?

var X = XElement.Parse (@" 
    <ROOT> 
     <MUL v='2' /> 
     <MUL v='3' /> 
    </ROOT> 
"); 
Enumerable.Range (1, 100) 
    .Select (s => X.Elements() 
     .Select (t => Int32.Parse (t.Attribute ("v").Value)) 
     .Aggregate (s, (t, u) => t * u) 
    ) 
    .ToList() 
    .ForEach (s => Console.WriteLine (s)); 

Was die .NET-Laufzeit hier eigentlich tut? Analysiert und konvertiert das System die Attribute 100-mal in Ganzzahlen oder ist es schlau genug, herauszufinden, ob es die analysierten Werte zwischenspeichern soll und nicht die Berechnung für jedes Element im Bereich wiederholen soll?

Darüber hinaus, wie würde ich selbst so etwas herausfinden?

Vielen Dank im Voraus für Ihre Hilfe.

+2

"Wie würde ich so etwas selbst herausfinden?" - das Beste ist, die IL zu studieren, die aus diesem Code generiert wird. – Andrey

+1

Sie könnten einen Debugger-Haltepunkt für die Parse() -Methode festlegen und sehen, wie oft er trifft. –

Antwort

2

Es ist eine Weile her, seit ich durch diesen Code gegraben habe, aber IIRC, die Art und Weise Select funktioniert, ist einfach zu speichern Func Sie liefern es und führen Sie es auf der Quellensammlung einzeln nacheinander. Also wird für jedes Element im äußeren Bereich die innere Select/Aggregate Sequenz so ausgeführt, als wäre es das erste Mal. Es gibt kein integriertes Caching - Sie müssten das selbst in den Ausdrücken implementieren.

Wenn Sie wollen dies selbst heraus Figur, haben Sie drei grundlegende Optionen bekommen:

  1. Kompilieren Sie den Code und ildasm verwenden, um die IL anzuzeigen; Es ist das genaueste, aber vor allem mit lambdas und closures, was du von IL bekommst, sieht vielleicht gar nicht so aus, wie du es in den C# -Compiler geschrieben hast.
  2. Verwenden Sie etwas wie dotPeek, um System.Linq.dll in C# zu dekompilieren; wieder, was Sie aus diesen Arten von Werkzeugen bekommen kann nur annähernd den ursprünglichen Quellcode, aber zumindest wird es C# (und insbesondere dotPeek macht einen ziemlich guten Job, und ist kostenlos.)
  3. Meine persönlichen Vorlieben - Laden Sie die .NET 4.0 Reference Source herunter und schauen Sie selbst; Das ist es, wofür es ist :) Sie müssen MS vertrauen, dass die Referenzquelle mit der tatsächlichen Quelle übereinstimmt, die zum Erzeugen der Binärdateien verwendet wurde, aber ich sehe keinen guten Grund, sie zu bezweifeln.
  4. Wie von @AllonGuralnek angegeben, können Sie Haltepunkte für bestimmte Lambda-Ausdrücke innerhalb einer einzelnen Zeile festlegen; Setzen Sie Ihren Cursor irgendwo in den Körper des Lambda und drücken Sie F9 und es wird nur das Lambda Breakpoint. (Wenn Sie es falsch macht, wird die gesamte Zeile in der Unterbrechungs Farbe markieren, wenn Sie es richtig machen, es wird nur das Lambda markiert.)
+0

Danke für die Antwort. Ich werde die erste und dritte Methode versuchen. – Shredderroy

+2

4. Setzen Sie den Cursor nach dem '=>' und drücken Sie F9. Das wird einen Breakpoint innerhalb des Lambda setzen und brechen, wenn er es erreicht. Wiederholen Sie für jedes Lambda und Sie erhalten eine schöne Spur von dem, was wann genannt wird. –

+0

@AllonGuralnek das ist ein guter Punkt, tendiere ich dazu, Breakpoint lambdas zu vergessen, weil ich normalerweise die Maus verwende, um sie zu setzen :) –

4

LINQ und IEnumerable<T> ist Pull basierte. Dies bedeutet, dass die Prädikate und Aktionen, die Teil der LINQ-Anweisung im Allgemeinen sind, erst ausgeführt werden, wenn Werte abgerufen werden. Darüber hinaus werden die Prädikate und Aktionen jedes Mal ausgeführt, wenn Werte abgerufen werden (z. B. gibt es kein geheimes Caching).

von einem IEnumerable<T> Pulling wird durch die foreach Aussage gemacht, die wirklich syntaktischen Zucker ist ein Enumerator für das Erhalten von IEnumerable<T>.GetEnumerator() Aufruf und IEnumerator<T>.MoveNext() Aufruf wiederholte die Werte zu ziehen.

LINQ-Operatoren wie ToList(), ToArray(), ToDictionary() und ToLookup() wickelt eine foreach Aussage, so dass diese Methoden einen Zug tun. Dasselbe kann über Operatoren wie Aggregate(), Count() und First() gesagt werden. Diese Methoden haben gemeinsam, dass sie ein einzelnes Ergebnis erzeugen, das durch Ausführen einer foreach-Anweisung erstellt werden muss.

Viele LINQ-Operatoren erzeugen eine neue IEnumerable<T> Sequenz. Wenn ein Element aus der resultierenden Sequenz gezogen wird, zieht der Operator ein oder mehrere Elemente aus der Quellsequenz. Der Operator Select() ist das offensichtlichste Beispiel, aber andere Beispiele sind SelectMany(), Where(), Concat(), Union(), Distinct(), Skip() und Take(). Diese Operatoren führen kein Caching durch. Wenn dann das N-te Element von einem Select() gezogen wird, zieht es das N-te Element aus der Quellensequenz, wendet die Projektion unter Verwendung der bereitgestellten Aktion an und gibt es zurück. Nichts Geheimnis hier.

Andere LINQ-Operatoren produzieren auch neue IEnumerable<T> Sequenzen, aber sie werden implementiert, indem die gesamte Quellsequenz tatsächlich gezogen wird, ihre Aufgabe erfüllt wird und dann eine neue Sequenz erzeugt wird. Diese Methoden umfassen Reverse(), OrderBy() und GroupBy(). Der Pull, der vom Operator ausgeführt wird, wird jedoch nur ausgeführt, wenn der Operator selbst gezogen wird, was bedeutet, dass Sie noch eine foreach-Schleife "am Ende" der LINQ-Anweisung benötigen, bevor irgendetwas ausgeführt wird. Sie könnten argumentieren, dass diese Operatoren einen Cache verwenden, weil sie sofort die gesamte Quellsequenz ziehen. Dieser Cache wird jedoch jedes Mal erstellt, wenn der Operator iteriert wird. Es handelt sich also wirklich um ein Implementierungsdetail und nicht um etwas, das auf magische Weise feststellt, dass Sie dieselbe Operation mehrmals in derselben Sequenz anwenden.


In Ihrem Beispiel wird die ToList() ziehen. Die Aktion im äußeren Select wird 100 Mal ausgeführt. Jedes Mal, wenn diese Aktion ausgeführt wird, führt die Aggregate() einen weiteren Pull durch, bei dem die XML-Attribute analysiert werden. Insgesamt wird Ihr Code Int32.Parse() 200 Mal aufrufen.

Sie können dies verbessern, indem die Attribute einmal ziehen, anstatt auf jeder Iteration:

var X = XElement.Parse (@" 
    <ROOT> 
     <MUL v='2' /> 
     <MUL v='3' /> 
    </ROOT> 
") 
.Elements() 
.Select (t => Int32.Parse (t.Attribute ("v").Value)) 
.ToList(); 
Enumerable.Range (1, 100) 
    .Select (s => x.Aggregate (s, (t, u) => t * u)) 
    .ToList() 
    .ForEach (s => Console.WriteLine (s)); 

Jetzt wird Int32.Parse() nur 2 mal aufgerufen. Die Kosten bestehen jedoch darin, dass eine Liste von Attributwerten zugeordnet, gespeichert und schließlich mit Müll gesammelt werden muss. (Keine große Sorge, wenn die Liste zwei Elemente enthält.)

Beachten Sie, dass wenn Sie die erste ToList() vergessen, die die Attribute zieht, der Code weiterhin ausgeführt wird, aber mit den gleichen Leistungsmerkmalen wie der ursprüngliche Code. Es wird kein Leerzeichen zum Speichern der Attribute verwendet, aber sie werden bei jeder Iteration analysiert.

+0

Vielen Dank für die ausführliche Antwort. – Shredderroy

Verwandte Themen