¿Se debe almacenar la longitud de la matriz en una variable local en C #?

Me doy cuenta de que la gente suele usar la construcción de esta manera:

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

Piensan que tener una llamada al Array.Length en cada iteración hará que CLR tome más tiempo para ejecutar el código. Para evitarlo, almacenan el valor de longitud en una variable local.
Averigüemos (¡de una vez por todas!) Si esto es algo viable o usar una variable temporal es una pérdida de tiempo.

Para comenzar, examinemos estos métodos de 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; } 

Así es como se ve procesado por el compilador JIT (para .NET Framework 4.7.2 bajo LegacyJIT-x86):
Sin variable ()
; int suma = 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) devuelve suma;
prueba ecx , ecx
salida jle
; int arrayLength2 = localRefToArray.Length;
mov eax , dword ptr [ edx + 4 ]
; if (i> = arrayLength2)
; lanzar nuevo IndexOutOfRangeException ();
bucle :
cmp esi , eax
jae 056e2d31
; suma + = localRefToArray [i];
agregar edi , dword ptr [ edx + esi * 4 + 8 ]
; i ++;
inc esi
; if (i <arrayLength) goto loop
cmp ecx , esi
jg loop
; suma de retorno;
salida :
mov eax , edi
WithVariable ()
; int suma = 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) devuelve suma;
prueba edi edi
salida jle
; int arrayLength2 = localRefToArray.Length;
mov ecx , dword ptr [ edx + 4 ]
; if (i> = arrayLength2)
; lanzar nuevo IndexOutOfRangeException ();
bucle :
cmp eax , ecx
jae 05902d31
; suma + = localRefToArray [i];
agregar esi , dword ptr [ edx + eax * 4 + 8 ]
; i ++;
inc eax
; if (i <arrayLength) goto loop
cmp eax edi
bucle jl
; suma de retorno;
salida :
mov eax , esi

Comparación en Meld:


Es trivial notar que tienen exactamente el mismo número de instrucciones de ensamblador: 15. Incluso la lógica de estas instrucciones es casi la misma. Hay una ligera diferencia en el orden de inicialización de las variables y las comparaciones sobre si el ciclo debe continuar. Podemos notar que en ambos casos la longitud de la matriz se registra dos veces antes del ciclo:

  • Para verificar 0 (arrayLength)
  • En la variable temporal para verificar la condición del ciclo (arrayLength2).

Resulta que ambos métodos se compilarán exactamente en el mismo código, pero el primero se escribe más rápido, a pesar de que no hay ningún beneficio en términos de tiempo de ejecución.
El código de ensamblador anterior me llevó a algunas reflexiones y decidí revisar un par de métodos más:

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

Ahora el elemento actual y la longitud de la matriz se suman, pero en el primer caso la longitud de la matriz se solicita cada vez, y en el segundo caso se guarda una vez en una variable local. Veamos el código de ensamblador de estos métodos:
Sin variable ()
int suma = 0 ;
xor edi , edi
int i = 0 ;
xor esi , esi
int [ ] localRefToArray = esto . matriz ;
mov edx , dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . Longitud ;
mov ebx , dword ptr [ edx + 4 ]
if ( arrayLength == 0 ) devuelve suma ;
prueba ebx , ebx
salida jle
int arrayLength2 = localRefToArray . Longitud ;
mov ecx , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 )
lanzar nuevo IndexOutOfRangeException ( ) ;
bucle :
cmp esi ecx
jae 05562d39
int t = matriz [ i ] ;
mov eax , dword ptr [ edx + esi * 4 + 8 ]
t + = suma ;
agregar eax , edi
t + = arrayLength ;
agregar eax , ebx
suma = t ;
mov edi , eax
i ++ ;
inc esi
if ( i <arrayLength ) goto loop
cmp ebx , esi
jg loop
suma de retorno ;
salida :
mov eax , edi
WithVariable ()
int suma = 0 ;
xor esi , esi
int [ ] localRefToArray = esto . matriz ;
mov edx , dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . Longitud ;
mov ebx , dword ptr [ edx + 4 ]
int i = 0 ;
xor ecx , ecx
if ( arrayLength == 0 ) ( suma de retorno ;)
prueba ebx , ebx
salida jle
int arrayLength2 = localRefToArray . Longitud ;
mov edi , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 )
lanzar nuevo IndexOutOfRangeException ( ) ;
bucle :
cmp ecx edi
jae 04b12d39
int t = matriz [ i ] ;
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
t + = suma ;
agregar eax , esi
t + = arrayLength ;
agregar eax , ebx
suma = t ;
mov esi , eax
i ++ ;
inc ecx
if ( i <arrayLength ) goto loop
cmp ecx , ebx
bucle jl
suma de retorno ;
salida :
mov eax , esi

Comparación en Meld:


Una vez más, el número de instrucciones es el mismo, así como (casi) las instrucciones mismas. La única diferencia es el orden de inicialización de las variables y la condición de verificación para la continuación del ciclo. Puede observar que en el cálculo de la suma, solo se tiene en cuenta la primera longitud de la matriz. Es obvio que esto:
int arrayLength2 = localRefToArray . Longitud ;
mov edi , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 ) arroja un nuevo IndexOutOfRangeException ( ) ;
cmp ecx edi
jae 04b12d39

en los cuatro métodos hay una comprobación de límites de matriz en línea y se ejecuta para cada elemento de la matriz.

Ya podemos llegar a la primera conclusión: usar una variable adicional para intentar acelerar el ciclo es una pérdida de tiempo, ya que el compilador lo hará por usted de todos modos. La única razón para almacenar una matriz de longitud en una variable es hacer que el código sea más legible.

Para cada uno es otra situación completamente. Considere los siguientes tres métodos:

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

Y aquí está el código después de JIT:

ForEachWithoutLength ()
; int suma = 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) va a salir;
prueba edi edi
salida jle
; int t = matriz [i];
bucle :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
; suma + = i;
agregar esi , eax
; i ++;
inc edx
; if (i <arrayLength) goto loop
cmp edi edx
jg loop
; suma de retorno;
salida :
mov eax , esi

ForEachWithLengthWithoutLocalVariable ()
; int suma = 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) va a salir
prueba edi edi
salida jle
; int t = matriz [i];
bucle :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
; suma + = i;
agregar esi , eax
; sum + = localRefToArray.Length;
agregar esi , dword ptr [ ecx + 4 ]
; i ++;
inc edx
; if (i <arrayLength) goto loop
cmp edi edx
jg loop
; suma de retorno;
salida :
mov eax , esi

ForEachWithLengthWithLocalVariable ()
; int suma = 0;
xor esi , esi
; int [] localRefToArray = this.array;
mov edx , dword ptr [ ecx + 4 ]
; int longitud = 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) va a salir;
prueba edi edi
salida jle
; int t = matriz [i];
bucle :
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
; suma + = i;
agregar esi , eax
; suma + = longitud;
agregar esi , ebx
; i ++;
inc ecx
; if (i <arrayLength) goto loop
cmp edi ecx
jg loop
; suma de retorno;
salida :
mov eax , esi

Lo primero que viene a la mente es que requiere menos instrucciones de ensamblador que el ciclo for (por ejemplo, para la suma simple de elementos, tomó 12 instrucciones en foreach , pero 15 en for ).
Comparación


En general, estos son los resultados de los parámetros comparativos de foreach para matrices de 1 millón de elementos:
 sum+=array[i]; 
Método
ItemsCount
Media
Error
Stddev
Mediana
Ratio
RatioSD
Foreach
1,000,000
1.401 ms
0.2691 ms
0,7935 ms
1.694 ms
1.00
0.00
Para
1,000,000
1.586 ms
0,3204 ms
0,9447 ms
1.740 ms
1,23
0,65
Y para
 sum+=array[i] + array.Length; 
Método
ItemsCount
Media
Error
Stddev
Mediana
Ratio
RatioSD
Foreach
1,000,000
1.703 ms
0,3010 ms
0,8874 ms
1.726 ms
1.00
0.00
Para
1,000,000
1.715 ms
0,2859 ms
0,8430 ms
1.956 ms
1.13
0,56

ForEach recorre la matriz mucho más rápido que para . Por qué Para averiguarlo, necesitamos comparar el código después de JIT:

Comparación de las tres opciones foreach


Veamos ForEachWithoutLength. La longitud de la matriz se solicita solo una vez y no hay comprobaciones para los límites de la matriz. Eso sucede porque el ciclo ForEach primero restringe el cambio de la colección dentro del ciclo, y el segundo nunca saldrá de la colección. Debido a eso, JIT puede permitirse eliminar los límites de la matriz de cheques.

Ahora echemos un vistazo a ForEachWithLengthWIthoutLocalVariable. Solo hay una parte extraña, donde suma + = longitud no sucede con la variable de longitud local previamente guardada, sino con una nueva que la aplicación solicita de la memoria cada vez. Eso significa que habrá N + 1 solicitudes de memoria para la longitud de la matriz, donde N es una longitud de la matriz.

Y ahora llegamos a ForEachWithLengthWithLocalVariable. El código allí es exactamente el mismo que en el ejemplo anterior, excepto el manejo de la longitud de la matriz. Una vez más, el compilador generó una variable variable localLongitud que se usa para verificar si la matriz está vacía, pero el compilador aún honestamente guardó nuestra longitud variable local indicada, y eso es lo que se usa en la suma dentro del ciclo. Resulta que este método solicita la longitud de la matriz de la memoria solo dos veces. La diferencia es muy difícil de notar en el mundo real.

En todos los casos, el código del ensamblador resultó tan simple porque los métodos en sí mismos son simples. Si los métodos tuvieran más parámetros, tendría que funcionar con la pila, las variables podrían tener almacenes fuera de los registros, habría habido más comprobaciones, pero la lógica principal seguiría siendo la misma: introducir una variable local para la longitud de la matriz es solo útil para hacer código más legible. También resultó que Foreach a menudo recorre la matriz más rápido que For .

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


All Articles