La longueur du tableau doit-elle être stockée dans une variable locale en C #?

Je remarque que les gens utilisent souvent des constructions comme celle-ci:

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

Ils pensent qu'avoir un appel à Array.Length à chaque itération fera que CLR prendra plus de temps pour exécuter le code. Pour l'éviter, ils stockent la valeur de longueur dans une variable locale.
Voyons (une fois pour toutes!) Si c'est une chose viable ou utiliser une variable temporaire est une perte de temps.

Pour commencer, examinons ces méthodes C #:

 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; } 

Voici à quoi il ressemble après avoir été traité par le compilateur JIT (pour .NET Framework 4.7.2 sous LegacyJIT-x86):
SansVariable ()
; 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) renvoie la somme;
test ecx , ecx
sortie jle
; int arrayLength2 = localRefToArray.Length;
mov eax , dword ptr [ edx + 4 ]
; if (i> = arrayLength2)
; lever une nouvelle IndexOutOfRangeException ();
boucle :
cmp esi , eax
jae 056e2d31
; sum + = localRefToArray [i];
ajouter edi , dword ptr [ edx + esi * 4 + 8 ]
; i ++;
inc esi
; if (i <arrayLength) goto loop
cmp ecx , esi
boucle jg
; somme de retour;
sortie :
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) renvoie la somme;
test edi edi
sortie jle
; int arrayLength2 = localRefToArray.Length;
mov ecx , dword ptr [ edx + 4 ]
; if (i> = arrayLength2)
; lever une nouvelle IndexOutOfRangeException ();
boucle :
cmp eax , ecx
jae 05902d31
; sum + = localRefToArray [i];
ajouter esi , dword ptr [ edx + eax * 4 + 8 ]
; i ++;
inc eax
; if (i <arrayLength) goto loop
cmp eax edi
boucle jl
; somme de retour;
sortie :
mov eax , esi

Comparaison à Meld:


Il est trivial de remarquer qu'ils ont exactement le même nombre d'instructions assembleur - 15. Même la logique de ces instructions est presque la même. Il y a une légère différence dans l'ordre d'initialisation des variables et des comparaisons sur la poursuite du cycle. On peut noter que dans les deux cas la longueur du tableau est enregistrée deux fois avant le cycle:

  • Pour vérifier 0 (arrayLength)
  • Dans la variable temporaire pour vérifier la condition du cycle (arrayLength2).

Il s'avère que les deux méthodes se compileront dans le même code exact, mais la première est écrite plus rapidement, même s'il n'y a aucun avantage en termes de temps d'exécution.
Le code assembleur ci-dessus m'a amené à quelques réflexions et j'ai décidé de vérifier quelques méthodes supplémentaires:

 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; } 

L'élément actuel et la longueur du tableau sont ajoutés, mais dans le premier cas, la longueur du tableau est demandée à chaque fois, et dans le second cas, elle est enregistrée une fois dans une variable locale. Regardons le code assembleur de ces méthodes:
SansVariable ()
int sum = 0 ;
xor edi , edi
int i = 0 ;
xor esi , esi
int [ ] localRefToArray = this . tableau ;
mov edx , dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . Longueur ;
mov ebx , dword ptr [ edx + 4 ]
if ( arrayLength == 0 ) renvoie la somme ;
test ebx , ebx
sortie jle
int arrayLength2 = localRefToArray . Longueur ;
mov ecx , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 )
lever une nouvelle IndexOutOfRangeException ( ) ;
boucle :
cmp esi ecx
jae 05562d39
int t = tableau [ i ] ;
mov eax , dword ptr [ edx + esi * 4 + 8 ]
t + = somme ;
ajouter eax , edi
t + = arrayLength ;
ajouter eax , ebx
somme = t ;
mov edi , eax
i ++ ;
inc esi
si ( i <arrayLength ) goto loop
cmp ebx , esi
boucle jg
somme de retour ;
sortie :
mov eax , edi
WithVariable ()
int sum = 0 ;
xor esi , esi
int [ ] localRefToArray = this . tableau ;
mov edx , dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . Longueur ;
mov ebx , dword ptr [ edx + 4 ]
int i = 0 ;
xor ecx , ecx
if ( arrayLength == 0 ) ( return sum ;)
test ebx , ebx
sortie jle
int arrayLength2 = localRefToArray . Longueur ;
mov edi , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 )
lever une nouvelle IndexOutOfRangeException ( ) ;
boucle :
cmp ecx edi
jae 04b12d39
int t = tableau [ i ] ;
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
t + = somme ;
ajouter eax , esi
t + = arrayLength ;
ajouter eax , ebx
somme = t ;
mov esi , eax
i ++ ;
inc ecx
si ( i <arrayLength ) goto loop
cmp ecx , ebx
boucle jl
somme de retour ;
sortie :
mov eax , esi

Comparaison à Meld:


Encore une fois, le nombre d'instructions est le même, ainsi que (presque) les instructions elles-mêmes. La seule différence est l'ordre d'initialisation des variables et la condition de contrôle pour la poursuite du cycle. Vous pouvez noter que dans le calcul de la somme, seule la première longueur du tableau est prise en compte. Il est évident que ceci:
int arrayLength2 = localRefToArray . Longueur ;
mov edi , dword ptr [ edx + 4 ]
si ( i> = arrayLength2 ), lancez new IndexOutOfRangeException ( ) ;
cmp ecx edi
jae 04b12d39

dans les quatre méthodes est une vérification des limites du tableau en ligne et elle est exécutée pour chaque élément du tableau.

Nous pouvons déjà faire la première conclusion: utiliser une variable supplémentaire pour essayer d'accélérer le cycle est une perte de temps, car le compilateur le fera quand même pour vous. La seule raison de stocker un tableau de longueur dans une variable est de rendre le code plus lisible.

ForEach est une autre situation entièrement. Considérez les trois méthodes suivantes:

 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; } 

Et voici le code après 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
sortie jle
; int t = tableau [i];
boucle :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
; somme + = i;
ajouter esi , eax
; i ++;
inc edx
; if (i <arrayLength) goto loop
cmp edi edx
boucle jg
; somme de retour;
sortie :
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) goto exit
test edi edi
sortie jle
; int t = tableau [i];
boucle :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
; somme + = i;
ajouter esi , eax
; sum + = localRefToArray.Length;
ajouter esi , dword ptr [ ecx + 4 ]
; i ++;
inc edx
; if (i <arrayLength) goto loop
cmp edi edx
boucle jg
; somme de retour;
sortie :
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
sortie jle
; int t = tableau [i];
boucle :
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
; somme + = i;
ajouter esi , eax
; somme + = longueur;
ajouter esi , ebx
; i ++;
inc ecx
; if (i <arrayLength) goto loop
cmp edi ecx
boucle jg
; somme de retour;
sortie :
mov eax , esi

La première chose qui me vient à l'esprit est qu'il faut moins d'instructions d'assembleur que pour le cycle (par exemple, pour la sommation d'éléments simples, il a fallu 12 instructions pour foreach , mais 15 pour for ).
Comparaison


Dans l'ensemble, voici les résultats du test de référence vs vs foreach pour les matrices à 1 million d'éléments:
 sum+=array[i]; 
La méthode
ItemsCount
Moyenne
Erreur
Stddev
Médiane
Ratio
RatioSD
Foreach
1 000 000
1,401 ms
0,2691 ms
0,7935 ms
1,694 ms
1,00
0,00
Pour
1 000 000
1,586 ms
0,3204 ms
0,9447 ms
1,740 ms
1,23
0,65
Et pour
 sum+=array[i] + array.Length; 
La méthode
ItemsCount
Moyenne
Erreur
Stddev
Médiane
Ratio
RatioSD
Foreach
1 000 000
1,703 ms
0,3010 ms
0,8874 ms
1,726 ms
1,00
0,00
Pour
1 000 000
1,715 ms
0,2859 ms
0,8430 ms
1,956 ms
1.13
0,56

ForEach parcourt le tableau beaucoup plus rapidement que pour . Pourquoi? Pour le savoir, nous devons comparer le code après JIT:

Comparaison des trois options foreach


Regardons ForEachWithoutLength. La longueur du tableau n'est demandée qu'une seule fois et il n'y a aucune vérification des limites du tableau. Cela se produit car le cycle ForEach restreint d'abord la modification de la collection à l'intérieur du cycle, et le second ne sortira jamais de la collection. Pour cette raison, JIT peut se permettre de supprimer les limites du tableau de contrôle.

Examinons maintenant attentivement ForEachWithLengthWIthoutLocalVariable. Il n'y a qu'une seule partie étrange, où la somme + + longueur arrive non pas à la longueur de tableau de variables locales précédemment enregistrée, mais à une nouvelle que l'application demande à la mémoire à chaque fois. Cela signifie qu'il y aura N + 1 demandes de mémoire pour la longueur du tableau, où N est une longueur du tableau.

Et maintenant, nous arrivons à ForEachWithLengthWithLocalVariable. Le code y est exactement le même que dans l'exemple précédent, à l'exception de la gestion de la longueur du tableau. Le compilateur a de nouveau généré une variable locale arrayLength qui est utilisée pour vérifier si le tableau est vide, mais le compilateur a quand même honnêtement enregistré notre longueur de variable locale indiquée, et c'est ce qui est utilisé dans la somme à l'intérieur du cycle. Il s'avère que cette méthode ne demande la longueur du tableau à la mémoire que deux fois. La différence est très difficile à remarquer dans le monde réel.

Dans tous les cas, le code assembleur s'est avéré si simple car les méthodes elles-mêmes sont simples. Si les méthodes avaient plus de paramètres, cela devrait fonctionner avec la pile, les variables pourraient obtenir des magasins en dehors des registres, il y aurait eu plus de vérifications, mais la logique principale resterait la même: l'introduction d'une variable locale pour la longueur du tableau n'est que utile pour rendre le code plus lisible. Il s'est également avéré que Foreach parcourt souvent le réseau plus rapidement que For .

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


All Articles