Die Diskussion um den Präferenzunterschied zwischen FOREACH und FOR ist nicht neu. Wir alle wissen, dass FOREACH langsamer ist, aber nicht alle wissen warum.
Als ich anfing, .NET zu lernen, sagte mir eine Person, dass FOREACH zweimal langsamer sei als FOR. Er sagte dies ohne jeden Grund. Ich hielt es für selbstverständlich.
Schließlich beschloss ich, den Leistungsunterschied zwischen FOREACH und FOR-Schleife zu untersuchen und diesen Artikel zu schreiben, um Nuancen zu diskutieren.
Schauen wir uns den folgenden Code an:
foreach (var item in Enumerable.Range(0, 128))
{
Console.WriteLine(item);
}
Der FOREACH ist ein Syntaxzucker. In diesem speziellen Fall wandelt der Compiler es in den folgenden Code um:
IEnumerator<int> enumerator = Enumerable.Range(0, 128).GetEnumerator();
try
{
while (enumerator.MoveNext())
{
int item = enumerator.Current;
Console.WriteLine(item);
}
}
finally
{
if (enumerator != null)
{
enumerator.Dispose();
}
}
Wenn wir das wissen, können wir den Grund dafür vermuten, warum FOREACH langsamer ist als FOR:
- Ein neues Objekt wird erstellt. Es heißt Creator.
- Die MoveNext-Methode wird bei jeder Iteration aufgerufen.
- Jede Iteration greift auf die Current-Eigenschaft zu.
Das ist es! Es ist jedoch nicht alles so einfach, wie es sich anhört.
Glücklicherweise (oder leider) kann C#/CLR zur Laufzeit Optimierungen durchführen. Der Vorteil ist, dass der Code schneller funktioniert. Die con – Entwickler sollten sich dieser Optimierungen bewusst sein.
Das Array ist ein tief in CLR integrierter Typ, und CLR bietet eine Reihe von Optimierungen für diesen Typ. Die FOREACH-Schleife ist eine iterierbare Entität, die ein Schlüsselaspekt der Leistung ist. Später in diesem Artikel werden wir besprechen, wie man mit Hilfe der statischen Array.ForEach-Methode und der List.ForEach-Methode durch Arrays und Listen iteriert.
Testmethoden
static double ArrayForWithoutOptimization(int[] array)
{
int sum = 0;
var watch = Stopwatch.StartNew();
for (int i = 0; i < array.Length; i++)
sum += array[i];
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
static double ArrayForWithOptimization(int[] array)
{
int length = array.Length;
int sum = 0;
var watch = Stopwatch.StartNew();
for (int i = 0; i < length; i++)
sum += array[i];
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
static double ArrayForeach(int[] array)
{
int sum = 0;
var watch = Stopwatch.StartNew();
foreach (var item in array)
sum += item;
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
static double ArrayForEach(int[] array)
{
int sum = 0;
var watch = Stopwatch.StartNew();
Array.ForEach(array, i => { sum += i; });
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
Testbedingungen:
- Die Option „Code optimieren“ ist aktiviert.
- Die Anzahl der Elemente ist gleich 100 000 000 (sowohl im Array als auch in der Liste).
- PC-Spezifikation:Intel Core i-5 und 8 GB RAM.
Arrays
Das Diagramm zeigt, dass FOR und FOREACH beim Durchlaufen von Arrays dieselbe Zeit benötigen. Und das liegt daran, dass die CLR-Optimierung FOREACH in FOR umwandelt und die Länge des Arrays als maximale Iterationsgrenze verwendet. Es spielt keine Rolle, ob die Array-Länge zwischengespeichert wird oder nicht (bei Verwendung von FOR), das Ergebnis ist fast dasselbe.
Es mag seltsam klingen, aber das Zwischenspeichern der Array-Länge kann die Leistung beeinträchtigen. Bei Verwendung von Array .Länge als Iterationsgrenze, JIT testet den Index, um über den Zyklus hinaus in die rechte Grenze zu treffen. Diese Prüfung wird nur einmal durchgeführt.
Es ist sehr einfach, diese Optimierung zu zerstören. Der Fall, dass die Variable zwischengespeichert wird, ist kaum optimiert.
Array.foreach zeigte die schlechtesten Ergebnisse. Seine Implementierung ist ganz einfach:
public static void ForEach<T>(T[] array, Action<T> action)
{
for (int index = 0; index < array.Length; ++index)
action(array[index]);
}
Warum läuft es dann so langsam? Es verwendet FOR unter der Haube. Nun, der Grund liegt im Aufrufen des ACTION-Delegierten. Tatsächlich wird bei jeder Iteration eine Methode aufgerufen, was die Leistung verringert. Außerdem werden die Delegierten nicht so schnell aufgerufen, wie wir möchten.
Listen
Das Ergebnis ist völlig anders. Beim Iterieren von Listen zeigen FOR und FOREACH unterschiedliche Ergebnisse. Es gibt keine Optimierung. FOR (mit Zwischenspeichern der Länge der Liste) zeigt das beste Ergebnis, während FOREACH mehr als 2-mal langsamer ist. Es liegt daran, dass es sich unter der Haube um MoveNext und Current handelt. Sowohl List.ForEach als auch Array.ForEach zeigen das schlechteste Ergebnis. Delegierte werden immer virtuell angerufen. Die Implementierung dieser Methode sieht folgendermaßen aus:
public void ForEach(Action<T> action)
{
int num = this._version;
for (int index = 0; index < this._size && num == this._version; ++index)
action(this._items[index]);
if (num == this._version)
return;
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
Jede Iteration ruft den Action-Delegaten auf. Es prüft auch, ob die Liste geändert wurde, und falls ja, wird eine Ausnahme geworfen.
List verwendet intern ein Array-basiertes Modell und die ForEach-Methode verwendet den Array-Index zum Durchlaufen, was erheblich schneller ist als die Verwendung des Indexers.
Spezifische Nummern
- Die FOR-Schleife ohne Längen-Caching und FOREACH arbeiten bei Arrays etwas schneller als FOR mit Längen-Caching.
- Array.Foreach Leistung ist ungefähr sechsmal langsamer als die Leistung von FOR / FOREACH.
- Die FOR-Schleife ohne Längen-Caching arbeitet bei Listen dreimal langsamer als bei Arrays.
- Die FOR-Schleife mit Längen-Caching arbeitet bei Listen doppelt so langsam wie bei Arrays.
- Die FOREACH-Schleife arbeitet bei Listen sechsmal langsamer als bei Arrays.
Hier ist eine Bestenliste für Listen:
Und für Arrays:
Schlussfolgerung
Ich habe diese Untersuchung wirklich genossen, insbesondere den Schreibprozess, und ich hoffe, Sie haben es auch genossen. Wie sich herausstellte, ist FOREACH bei Arrays schneller als FOR mit Längenverfolgung. Bei Listenstrukturen ist FOREACH langsamer als FOR.
Der Code sieht besser aus, wenn FOREACH verwendet wird, und moderne Prozessoren ermöglichen die Verwendung. Wenn Sie jedoch Ihre Codebasis stark optimieren müssen, ist es besser, FOR.
zu verwendenWas meint ihr, welche Schleife läuft schneller, FOR oder FOREACH?