Ist Linq effizienter als es auf der Oberfläche erscheint?

13

Wenn ich so etwas schreibe:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue)

Ist das dasselbe wie:

var results1 = new List<Thing>();
foreach(var t in mythings)
    if(t.IsSomeValue)
        results1.Add(t);

var results2 = new List<Thing>();
foreach(var t in results1)
    if(t.IsSomeOtherValue)
        results2.Add(t);

Oder gibt es etwas Magie unter der Decke, die eher so funktioniert:

var results = new List<Thing>();
foreach(var t in mythings)
    if(t.IsSomeValue && t.IsSomeOtherValue)
        results.Add(t);

Oder ist es etwas ganz anderes?

ConditionRacer
quelle
4
Sie können dies in ILSpy anzeigen.
ChaosPandion
1
Es ist eher das zweite Beispiel als die erste, aber zweite Antwort von ChaosPandion, dass ILSpy dein Freund ist.
Michael
2
Siehe auch Warum übertreffen Where and Select nur Select?
BlueRaja - Danny Pflughoeft

Antworten:

27

LINQ-Abfragen sind faul . Das heißt der Code:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue);

tut sehr wenig. Das ursprüngliche enumerable ( mythings) wird nur dann aufgezählt, wenn das resultierende enumerable ( things) verbraucht wird, z. B. von einer foreachSchleife .ToList(), oder .ToArray().

Wenn Sie aufrufen things.ToList(), entspricht dies in etwa dem zuletzt genannten Code, möglicherweise mit einigem (normalerweise unbedeutendem) Overhead durch die Enumeratoren.

Ebenso, wenn Sie eine foreach-Schleife verwenden:

foreach (var t in things)
    DoSomething(t);

Es ist in der Leistung ähnlich zu:

foreach (var t in mythings)
    if (t.IsSomeValue && t.IsSomeOtherValue)
        DoSomething(t);

Einige der Leistungsvorteile des Laziness-Ansatzes für Enumerables (im Gegensatz zum Berechnen aller Ergebnisse und zum Speichern in einer Liste) bestehen darin, dass nur sehr wenig Arbeitsspeicher belegt wird (da jeweils nur ein Ergebnis gespeichert wird) und keine signifikanten Fehler auftreten Kosten.

Wenn die Aufzählung nur teilweise erfolgt, ist dies besonders wichtig. Betrachten Sie diesen Code:

things.First();

Die Art und Weise, wie LINQ implementiert wird, mythingswird nur bis zum ersten Element aufgezählt, das Ihren where-Bedingungen entspricht. Befindet sich dieses Element zu Beginn der Liste, kann dies eine enorme Leistungssteigerung bedeuten (z. B. O (1) anstelle von O (n)).

Cyanfisch
quelle
1
Ein Leistungsunterschied zwischen LINQ und dem entsprechenden Code foreachbesteht darin, dass LINQ Delegate-Aufrufe verwendet, die einen gewissen Overhead haben. Dies kann von Bedeutung sein, wenn die Bedingungen sehr schnell ausgeführt werden (was häufig der Fall ist).
Svick
2
Das habe ich mit Enumerator-Overhead gemeint. Dies kann in einigen (seltenen) Fällen ein Problem sein, aber meiner Erfahrung nach ist dies nicht sehr häufig. Normalerweise ist der Zeitaufwand anfangs sehr gering oder wird von anderen von Ihnen durchgeführten Operationen bei weitem aufgewogen.
Cyanfish
Eine unangenehme Einschränkung von Linqs fauler Bewertung ist, dass es keine Möglichkeit gibt, einen "Schnappschuss" einer Aufzählung zu machen, außer über Methoden wie ToListoder ToArray. Wenn so etwas richtig eingebaut worden wäre IEnumerable, wäre es möglich gewesen, eine Liste anzufordern, um alle Aspekte, die sich in der Zukunft ändern könnten, zu "schnappschießen", ohne alles generieren zu müssen.
Supercat
7

Der folgende Code:

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue);

Ist gleichbedeutend mit nichts, wegen der faulen Bewertung wird nichts passieren.

var things = mythings
    .Where(x => x.IsSomeValue)
    .Where(y => y.IsSomeOtherValue)
    .ToList();

Ist anders, weil die Auswertung gestartet wird.

Jeder Gegenstand mythingswird dem ersten gegeben Where. Wenn es besteht, wird es auf die Sekunde gegeben Where. Wenn es erfolgreich ist, wird es Teil der Ausgabe.

Das sieht also ungefähr so ​​aus:

var results = new List<Thing>();
foreach(var t in mythings)
{
    if(t.IsSomeValue)
    {
        if(t.IsSomeOtherValue)
        {
            results.Add(t);
        }
    }
}
Cyril Gandon
quelle
7

Abgesehen von der verzögerten Ausführung (auf die in den anderen Antworten bereits eingegangen wird, ich möchte nur auf ein weiteres Detail hinweisen) ist dies eher in Ihrem zweiten Beispiel der Fall.

Stellen wir uns vor, Sie rufen ToListan things.

Die Umsetzung von Enumerable.WhereRetouren a Enumerable.WhereListIterator. Wenn Sie rufen Whereauf , dass WhereListIterator(auch bekannt als Verkettungs Where-calls), die Sie nicht mehr Anruf Enumerable.Where, aber Enumerable.WhereListIterator.Where, die die Prädikate tatsächlich kombiniert (mit Enumerable.CombinePredicates).

Also ist es eher so if(t.IsSomeValue && t.IsSomeOtherValue).

Faultier
quelle
"Gibt einen Enumerable.WhereListIterator zurück" hat es für mich zum Klicken gebracht. Wahrscheinlich ein sehr einfaches Konzept, aber genau das habe ich mit ILSpy übersehen. Vielen Dank
ConditionRacer
Sehen Sie sich Jon Skeets Neuimplementierung dieser Optimierung an, wenn Sie an einer eingehenderen Analyse interessiert sind.
Servy
1

Nein, es ist nicht dasselbe. In Ihrem Beispiel thingsist ein IEnumerable, der zu diesem Zeitpunkt noch nur ein Iterator ist, kein tatsächliches Array oder eine Liste. Darüber hinaus thingswird die Schleife niemals ausgewertet, da sie nicht verwendet wird. Der Typ IEnumerableermöglicht es, Elemente, die yieldvon Linq-Anweisungen erstellt wurden, zu durchlaufen und mit weiteren Anweisungen weiterzuverarbeiten, was bedeutet, dass Sie letztendlich nur eine einzige Schleife haben.

Sobald Sie jedoch eine Anweisung wie .ToArray()oder hinzufügen .ToList(), ordnen Sie die Erstellung einer tatsächlichen Datenstruktur an und setzen so Ihrer Kette Grenzen.

Siehe diese verwandte SO-Frage: /programming/2789389/how-do-i-implement-ienumerable

Julien Guertault
quelle