Warum ist LINQ .Where (Prädikat) .First () schneller als .First (Prädikat)?

73

Ich mache einige Leistungstests und habe festgestellt, dass ein LINQ-Ausdruck gefällt

result = list.First(f => f.Id == i).Property

ist langsamer als

result = list.Where(f => f.Id == i).First().Property

Dies scheint nicht intuitiv zu sein. Ich hätte gedacht, dass der erste Ausdruck schneller sein würde, da er die Iteration über die Liste beenden kann, sobald das Prädikat erfüllt ist, während ich gedacht hätte, dass der .Where()Ausdruck die gesamte Liste durchlaufen könnte, bevor .First()die resultierende Teilmenge aufgerufen wird. Selbst wenn letzterer einen Kurzschluss macht, sollte es nicht schneller sein als First direkt zu verwenden, aber es ist.

Im Folgenden finden Sie zwei wirklich einfache Komponententests, die dies veranschaulichen. Beim Kompilieren mit Optimierung auf TestWhereAndFirst ist es ungefähr 30% schneller als auf TestFirstOnly unter .Net und Silverlight 4. Ich habe versucht, das Prädikat dazu zu bringen, mehr Ergebnisse zurückzugeben, aber der Leistungsunterschied ist der gleiche.

Kann jemand erklären, warum .First(fn)langsamer ist als .Where(fn).First()? Ich sehe ein ähnliches kontraintuitives Ergebnis im .Count(fn)Vergleich zu .Where(fn).Count().

private const int Range = 50000;

private class Simple
{
   public int Id { get; set; }
   public int Value { get; set; }
}

[TestMethod()]
public void TestFirstOnly()
{
   List<Simple> list = new List<Simple>(Range);
   for (int i = Range - 1; i >= 0; --i)
   {
      list.Add(new Simple { Id = i, Value = 10 });
   }

   int result = 0;
   for (int i = 0; i < Range; ++i)
   {
      result += list.First(f => f.Id == i).Value;
   }

   Assert.IsTrue(result > 0);
}

[TestMethod()]
public void TestWhereAndFirst()
{
   List<Simple> list = new List<Simple>(Range);
   for (int i = Range - 1; i >= 0; --i)
   {
      list.Add(new Simple { Id = i, Value = 10 });
   }

   int result = 0;
   for (int i = 0; i < Range; ++i)
   {
      result += list.Where(f => f.Id == i).First().Value;
   }

   Assert.IsTrue(result > 0);
}
dazza
quelle
5
Ihr anfänglicher Gedanke ist jedoch falsch: LINQ berechnet faul. Wenn First()es aufgerufen wird, Where(...)fragt es (den Rückgabewert von) nur nach einer Übereinstimmung und fragt niemals nach einer anderen. Es wird also genau die gleiche Anzahl von Elementen untersucht wie beim Aufrufen First(...)(dh direkt mit einem Prädikat).
Jon
1
Ich bekomme das gleiche Ergebnis, .Where().First()ist .021 Sekunden und .First()ist .037 Sekunden. Dies ist mit einer einfachen Liste von ints.
Ry-
Gemäß meinem Test hängt es auch davon ab, nach welchem ​​Element Sie suchen. Versuchen Sie es einfach mit einem bestimmten i-Wert, wenn Sie Wo und erstes Prädikat anwenden. Ich versuche es mit Wert 1 und später 4999. Ich sehe Unterschiede im Ergebnis. Es scheint, dass First jedes Element durchläuft und für ein bestimmtes Prädikat übereinstimmt, bis es übereinstimmt.
Dotnetstep
5
@minitech Du hast Reset()deine Stoppuhr nicht angerufen ; Ihr Test zeigt tatsächlich, dass First()es deutlich schneller ist.
Jay

Antworten:

50

Ich habe die gleichen Ergebnisse erzielt: wobei + first schneller war als first.

Wie Jon bemerkte, verwendet Linq eine verzögerte Bewertung, sodass die Leistung für beide Methoden weitgehend ähnlich sein sollte (und ist).

In Reflector verwendet First zunächst eine einfache foreach-Schleife, um die Sammlung zu durchlaufen. Dabei gibt es eine Vielzahl von Iteratoren, die auf verschiedene Sammlungstypen (Arrays, Listen usw.) spezialisiert sind. Vermutlich gibt dies Where den kleinen Vorteil.

arx
quelle
11
Aber wenn ich ein Framework-Entwickler wäre und First (fn) nur intern als return Where (fn) .First () implementiert hätte, würde es genau wie die aktuelle Implementierung von First funktionieren, außer schneller! Scheint ein schlechtes Versehen von Microsoft zu sein.
Dazza
1
Ziemlich. Ich denke, niemand hat jemals darüber nachgedacht.
Arx
5
Vergleichen Sie auch .Count (fn) mit .Where (fn) .Count (). Letzteres ist aufgrund der Verwendung spezialisierter Iteratoren schneller als der von .Count (fn) verwendeten foreach. Die Verwendung von Convenience-Methoden wie .First (fn) und .Count (fn) führt zu einem präziseren Code und scheint daher das Richtige zu sein tun, aber die .Where (fn) .Method () ist messbar schneller. Grrrr!
Dazza
5
Werfen Sie einen Blick auf dotnetfiddle.net/k11nX6 , Sie werden überrascht sein, dass die Antwort falsch ist.
Akash Kava
7
Versuche das jetzt !!! dotnetfiddle.net/OrUUSG , deine Antwort ist falsch !!! Probieren Sie eine beliebige Kombination aus und beweisen Sie, dass Ihre Antwort richtig ist. Die eigentliche Antwort lautet: Wo enumerable Caches aufzählbar sind. Enumerable ist ein Vorteil gegenüber dem alten List / Array-Iterator, hat aber nichts mit dem zu tun, was schnell ist.
Akash Kava