Sollte die Arraylänge in einer lokalen Variablen in C # gespeichert werden?

Ich stelle fest, dass die Leute oft so konstruieren:

var length = array.Length; for (int i = 0; i < length; i++) {    //do smth } 

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):
WithoutVariable ()
; int sum = 0;
xor edi , edi
; int i = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov edx , dword ptr [ ecx + 4 ]
; int arrayLength = localRefToArray.Length;
mov ecx , dword ptr [ edx + 4 ]
; if (arrayLength == 0) return sum;
test ecx , ecx
jle Ausgang
; int arrayLength2 = localRefToArray.Length;
mov eax , dword ptr [ edx + 4 ]
; if (i> = arrayLength2)
;; neue IndexOutOfRangeException () auslösen;
Schleife :
cmp esi , eax
jae 056e2d31
; sum + = localRefToArray [i];
füge edi hinzu , dword ptr [ edx + esi * 4 + 8 ]
; i ++;
inc esi
; if (i <arrayLength) gehe zur Schleife
cmp ecx , esi
JG- Schleife
; Rückgabesumme;
Ausfahrt :
mov eax , edi
WithVariable ()
; int sum = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov edx , dword ptr [ ecx + 4 ]
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ edx + 4 ]
; int i = 0;
xor eax , eax
; if (arrayLength == 0) return sum;
test edi edi
jle Ausgang
; int arrayLength2 = localRefToArray.Length;
mov ecx , dword ptr [ edx + 4 ]
; if (i> = arrayLength2)
;; neue IndexOutOfRangeException () auslösen;
Schleife :
cmp eax , ecx
jae 05902d31
; sum + = localRefToArray [i];
füge esi hinzu , dword ptr [ edx + eax * 4 + 8 ]
; i ++;
inc eax
; if (i <arrayLength) gehe zur Schleife
cmp eax edi
jl Schleife
; Rückgabesumme;
Ausfahrt :
mov eax , esi

Vergleich in Meld:


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:
WithoutVariable ()
int sum = 0 ;
xor edi , edi
int i = 0 ;
xor esi , esi
int [ ] localRefToArray = this . Array ;
mov edx , dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . Länge ;
mov ebx , dword ptr [ edx + 4 ]
if ( arrayLength == 0 ) return sum ;
teste ebx , ebx
jle Ausgang
int arrayLength2 = localRefToArray . Länge ;
mov ecx , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 )
neue IndexOutOfRangeException ( ) auslösen ;
Schleife :
cmp esi ecx
jae 05562d39
int t = Array [ i ] ;
mov eax , dword ptr [ edx + esi * 4 + 8 ]
t + = Summe ;
füge eax , edi hinzu
t + = arrayLength ;
füge eax , ebx hinzu
Summe = t ;
mov edi , eax
i ++ ;
inc esi
if ( i <arrayLength ) gehe zur Schleife
cmp ebx , esi
JG- Schleife
Rückgabesumme ;
Ausfahrt :
mov eax , edi
WithVariable ()
int sum = 0 ;
xor esi , esi
int [ ] localRefToArray = this . Array ;
mov edx , dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . Länge ;
mov ebx , dword ptr [ edx + 4 ]
int i = 0 ;
xor ecx , ecx
if ( arrayLength == 0 ) ( Rückgabesumme ;)
teste ebx , ebx
jle Ausgang
int arrayLength2 = localRefToArray . Länge ;
mov edi , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 )
neue IndexOutOfRangeException ( ) auslösen ;
Schleife :
cmp ecx edi
jae 04b12d39
int t = Array [ i ] ;
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
t + = Summe ;
füge eax hinzu , esi
t + = arrayLength ;
füge eax , ebx hinzu
Summe = t ;
mov esi , eax
i ++ ;
inc ecx
if ( i <arrayLength ) gehe zur Schleife
cmp ecx , ebx
jl Schleife
Rückgabesumme ;
Ausfahrt :
mov eax , esi

Vergleich in Meld:


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 ).
Vergleich


Insgesamt sind hier die Ergebnisse von for vs foreach- Benchmark für Arrays mit 1 Million Elementen:
 sum+=array[i]; 
Methode
ItemsCount
Mittelwert
Fehler
Stddev
Median
Verhältnis
RatioSD
Foreach
1.000.000
1,401 ms
0,2691 ms
0,7935 ms
1,694 ms
1,00
0,00
Für
1.000.000
1,586 ms
0,3204 ms
0,9447 ms
1,740 ms
1.23
0,65
Und für
 sum+=array[i] + array.Length; 
Methode
ItemsCount
Mittelwert
Fehler
Stddev
Median
Verhältnis
RatioSD
Foreach
1.000.000
1,703 ms
0,3010 ms
0,8874 ms
1,726 ms
1,00
0,00
Für
1.000.000
1,715 ms
0,2859 ms
0,8430 ms
1,956 ms
1.13
0,56

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.

Source: https://habr.com/ru/post/de454582/


All Articles