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++) {
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):
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:
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 ).
En general, estos son los resultados de los parámetros comparativos de
foreach para matrices de 1 millón de elementos:
sum+=array[i];
Y para
sum+=array[i] + array.Length;
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 .