Ich stelle fest, dass die Leute oft so konstruieren:
var length = array.Length; for (int i = 0; i < length; i++) {
Sie glauben, dass ein Aufruf der Array.Length bei jeder Iteration dazu führt, dass die CLR mehr Zeit benötigt, um den Code auszuführen. Um dies zu vermeiden, speichern sie den Längenwert in einer lokalen Variablen.
Lassen Sie uns herausfinden (ein für alle Mal!), Ob dies sinnvoll ist oder ob die Verwendung einer temporären Variablen Zeitverschwendung ist.
Lassen Sie uns zunächst diese C # -Methoden untersuchen:
public int WithoutVariable() { int sum = 0; for (int i = 0; i < array.Length; i++) { sum += array[i]; } return sum; } public int WithVariable() { int sum = 0; int length = array.Length; for (int i = 0; i < length; i++) { sum += array[i]; } return sum; }
So sieht es aus, nachdem es vom JIT-Compiler verarbeitet wurde (für .NET Framework 4.7.2 unter LegacyJIT-x86):
Es ist trivial zu bemerken, dass sie genau die gleiche Anzahl von Assembler-Anweisungen haben - 15. Sogar die Logik dieser Anweisungen ist fast dieselbe. Es gibt einen kleinen Unterschied in der Reihenfolge der Initialisierung von Variablen und der Vergleiche, ob der Zyklus fortgesetzt werden soll. Wir können feststellen, dass in beiden Fällen die Array-Länge zweimal vor dem Zyklus registriert wird:
- Um nach 0 zu suchen (arrayLength)
- In die temporäre Variable zum Überprüfen der Zyklusbedingung (arrayLength2).
Es stellt sich heraus, dass beide Methoden in genau denselben Code kompiliert werden, aber die erste wird schneller geschrieben, obwohl es keinen Vorteil hinsichtlich der Ausführungszeit gibt.
Der obige Assembler-Code führte mich zu einigen Gedanken und ich beschloss, ein paar weitere Methoden zu überprüfen:
public int WithoutVariable() { int sum = 0; for(int i = 0; i < array.Length; i++) { sum += array[i] + array.Length; } return sum; } public int WithVariable() { int sum = 0; int length = array.Length; for(int i = 0; i < length; i++) { sum += array[i] + length; } return sum; }
Jetzt werden das aktuelle Element und die Array-Länge addiert, aber im ersten Fall wird die Array-Länge jedes Mal angefordert und im zweiten Fall wird sie einmal in einer lokalen Variablen gespeichert. Schauen wir uns den Assembler-Code dieser Methoden an:
Auch hier sind die Anzahl der Anweisungen und (fast) die Anweisungen selbst gleich. Der einzige Unterschied besteht in der Reihenfolge der Initialisierung der Variablen und der Prüfbedingung für die Fortsetzung des Zyklus. Sie können feststellen, dass bei der Berechnung der Summe nur die erste Länge des Arrays berücksichtigt wird. Es ist offensichtlich, dass dies:
int arrayLength2 = localRefToArray . Länge ;
mov edi , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 ) löst eine neue IndexOutOfRangeException ( ) aus ;
cmp ecx edi
jae 04b12d39
Bei allen vier Methoden wird ein Inline-Array überprüft und für jedes Element des Arrays ausgeführt.
Wir können bereits die erste Schlussfolgerung ziehen: Die Verwendung einer zusätzlichen Variablen, um den Zyklus zu beschleunigen, ist Zeitverschwendung, da der Compiler dies sowieso für Sie erledigt. Der einzige Grund, ein Längenarray in einer Variablen zu speichern, besteht darin, den Code besser lesbar zu machen.
ForEach ist eine ganz andere Situation. Betrachten Sie die folgenden drei Methoden:
public int ForEachWithoutLength() { int sum = 0; foreach (int i in array) { sum += i; } return sum; } public int ForEachWithLengthWithoutLocalVariable() { int sum = 0; foreach (int i in array) { sum += i + array.Length; } return sum; } public int ForEachWithLengthWithLocalVariable() { int sum = 0; int length = array.Length; foreach (int i in array) { sum += i + length; } return sum; }
Und hier ist der Code nach JIT:
ForEachWithoutLength (); int sum = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov ecx , dword ptr [ ecx + 4 ]
; int i = 0;
xor edx , edx
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ ecx + 4 ]
; if (arrayLength == 0) goto exit;
test edi edi
jle Ausgang
; int t = Array [i];
Schleife :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
; Summe + = i;
füge esi , eax hinzu
; i ++;
inc edx
; if (i <arrayLength) gehe zur Schleife
cmp edi edx
JG- Schleife
; Rückgabesumme;
Ausfahrt :
mov eax , esi
ForEachWithLengthWithoutLocalVariable (); int sum = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov ecx , dword ptr [ ecx + 4 ]
; int i = 0;
xor edx , edx
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ ecx + 4 ]
; if (arrayLength == 0) gehe zum Beenden
test edi edi
jle Ausgang
; int t = Array [i];
Schleife :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
; Summe + = i;
füge esi , eax hinzu
; sum + = localRefToArray.Length;
füge esi hinzu , dword ptr [ ecx + 4 ]
; i ++;
inc edx
; if (i <arrayLength) gehe zur Schleife
cmp edi edx
JG- Schleife
; Rückgabesumme;
Ausfahrt :
mov eax , esi
ForEachWithLengthWithLocalVariable (); int sum = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov edx , dword ptr [ ecx + 4 ]
; int length = localRefToArray.Length;
mov ebx , dword ptr [ edx + 4 ]
; int i = 0;
xor ecx , ecx
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ edx + 4 ]
; if (arrayLength == 0) goto exit;
test edi edi
jle Ausgang
; int t = Array [i];
Schleife :
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
; Summe + = i;
füge esi , eax hinzu
; Summe + = Länge;
füge esi , ebx hinzu
; i ++;
inc ecx
; if (i <arrayLength) gehe zur Schleife
cmp edi ecx
JG- Schleife
; Rückgabesumme;
Ausfahrt :
mov eax , esi
Das erste, was mir in den Sinn kommt, ist, dass weniger Assembler-Anweisungen als
für den
for- Zyklus erforderlich sind (für die einfache Elementsummierung wurden beispielsweise 12 Anweisungen in
foreach , aber 15 in
for benötigt ).
Insgesamt sind hier die Ergebnisse von
for vs foreach- Benchmark für Arrays mit 1 Million Elementen:
sum+=array[i];
Und für
sum+=array[i] + array.Length;
ForEach geht viel schneller durch das Array als
for . Warum? Um dies herauszufinden, müssen wir den Code nach JIT vergleichen:
Vergleich aller drei foreach-Optionen Schauen wir uns ForEachWithoutLength an. Die Array-Länge wird nur einmal angefordert, und die Array-Grenzen werden nicht überprüft. Dies geschieht, weil der ForEach-Zyklus erstens das Ändern der Sammlung innerhalb des Zyklus einschränkt und der zweite niemals die Sammlung verlässt. Aus diesem Grund kann es sich JIT leisten, die Grenzen des Prüfarrays zu entfernen.
Schauen wir uns nun ForEachWithLengthWIthoutLocalVariable genau an. Es gibt nur einen seltsamen Teil, bei dem sum + = length nicht für die zuvor gespeicherte lokale Variable arrayLength gilt, sondern für einen neuen, den die App jedes Mal aus dem Speicher anfordert. Das heißt, es gibt N + 1 Speicheranforderungen für die Arraylänge, wobei N eine Arraylänge ist.
Und jetzt kommen wir zu ForEachWithLengthWithLocalVariable. Der Code dort ist genau der gleiche wie im vorherigen Beispiel, außer dass die Array-Länge behandelt wird. Der Compiler hat erneut eine lokale Variable arrayLength generiert, mit der überprüft wird, ob das Array leer ist. Der Compiler hat jedoch die angegebene Länge der lokalen Variablen ehrlich gespeichert, und dies wird in der Summierung innerhalb des Zyklus verwendet. Es stellt sich heraus, dass diese Methode die Array-Länge nur zweimal aus dem Speicher anfordert. Der Unterschied ist in der realen Welt sehr schwer zu bemerken.
In allen Fällen stellte sich Assembler-Code als so einfach heraus, weil die Methoden selbst einfach sind. Wenn die Methoden mehr Parameter hätten, müsste sie mit dem Stapel funktionieren, Variablen könnten Speicher außerhalb der Register erhalten, es hätte mehr Überprüfungen gegeben, aber die Hauptlogik würde dieselbe bleiben: Die Einführung einer lokalen Variablen für die Array-Länge ist nur möglich nützlich, um besser lesbaren Code zu erstellen. Es stellte sich auch heraus, dass
Foreach häufig schneller als
For durch das Array läuft.