O comprimento da matriz deve ser armazenado em uma variável local em C #?

Percebo que as pessoas costumam usar construções como esta:

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

Eles acham que ter uma chamada para o Array.Length em cada iteração fará com que o CLR demore mais tempo para executar o código. Para evitá-lo, eles armazenam o valor do comprimento em uma variável local.
Vamos descobrir (de uma vez por todas!) Se isso é viável ou o uso de uma variável temporária é uma perda de tempo.

Para começar, vamos examinar estes métodos 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; } 

Aqui está como ele é processado pelo compilador JIT (para .NET Framework 4.7.2 em LegacyJIT-x86):
WithoutVariable ()
int soma = 0;
xor edi , edi
int = 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) retorna soma;
teste ecx , ecx
saída jle
; int arrayLength2 = localRefToArray.Length;
mov eax , dword ptr [ edx + 4 ]
; if (i> = arrayLength2)
; lançar novo IndexOutOfRangeException ();
loop :
cmp esi , eax
jae 056e2d31
; soma + = localRefToArray [i];
adicione edi , dword ptr [ edx + esi * 4 + 8 ]
; i ++;
inc esi
; if (i <arrayLength) goto loop
cmp ecx , esi
jg loop
soma de retorno;
saída :
mov eax , edi
WithVariable ()
int soma = 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 = 0;
xor eax , eax
; if (arrayLength == 0) retorna soma;
teste edi edi
saída jle
; int arrayLength2 = localRefToArray.Length;
mov ecx , dword ptr [ edx + 4 ]
; if (i> = arrayLength2)
; lançar novo IndexOutOfRangeException ();
loop :
cmp eax , ecx
jae 05902d31
; soma + = localRefToArray [i];
adicione esi , dword ptr [ edx + eax * 4 + 8 ]
; i ++;
inc eax
; if (i <arrayLength) goto loop
cmp eax edi
jl loop
soma de retorno;
saída :
mov eax , esi

Comparação em Meld:


É trivial notar que eles têm exatamente o mesmo número de instruções do assembler - 15. Mesmo a lógica dessas instruções é quase a mesma. Há uma pequena diferença na ordem de inicialização das variáveis ​​e comparações sobre se o ciclo deve continuar. Podemos observar que, em ambos os casos, o comprimento da matriz é registrado duas vezes antes do ciclo:

  • Para verificar se há 0 (arrayLength)
  • Na variável temporária para verificar a condição do ciclo (arrayLength2).

Acontece que ambos os métodos serão compilados exatamente no mesmo código, mas o primeiro é escrito mais rapidamente, mesmo que não haja nenhum benefício em termos de tempo de execução.
O código do assembler acima me levou a algumas reflexões e eu decidi verificar mais alguns métodos:

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

Agora, o elemento atual e o comprimento da matriz estão sendo somados, mas no primeiro caso, o comprimento da matriz é solicitado sempre e, no segundo caso, é salvo uma vez em uma variável local. Vamos dar uma olhada no código do assembler desses métodos:
WithoutVariable ()
int soma = 0 ;
xor edi , edi
int i = 0 ;
xor esi , esi
int [ ] localRefToArray = isso . matriz ;
mov edx , dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . Comprimento ;
mov ebx , dword ptr [ edx + 4 ]
if ( arrayLength == 0 ) retorna soma ;
teste ebx , ebx
saída jle
int arrayLength2 = localRefToArray . Comprimento ;
mov ecx , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 )
lançar novo IndexOutOfRangeException ( ) ;
loop :
cmp esi ecx
jae 05562d39
int t = matriz [ i ] ;
mov eax , dword ptr [ edx + esi * 4 + 8 ]
t + = soma ;
adicionar eax , edi
t + = arrayLength ;
adicionar eax , ebx
soma = t ;
mov edi , eax
i ++ ;
inc esi
if ( i <arrayLength ) goto loop
cmp ebx , esi
jg loop
soma de retorno ;
saída :
mov eax , edi
WithVariable ()
int soma = 0 ;
xor esi , esi
int [ ] localRefToArray = isso . matriz ;
mov edx , dword ptr [ ecx + 4 ]
int arrayLength = localRefToArray . Comprimento ;
mov ebx , dword ptr [ edx + 4 ]
int i = 0 ;
xor ecx , ecx
if ( arrayLength == 0 ) ( retornar soma ;)
teste ebx , ebx
saída jle
int arrayLength2 = localRefToArray . Comprimento ;
mov edi , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 )
lançar novo IndexOutOfRangeException ( ) ;
loop :
cmp ecx edi
jae 04b12d39
int t = matriz [ i ] ;
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
t + = soma ;
adicionar eax , esi
t + = arrayLength ;
adicionar eax , ebx
soma = t ;
mov esi , eax
i ++ ;
inc ecx
if ( i <arrayLength ) goto loop
cmp ecx , ebx
jl loop
soma de retorno ;
saída :
mov eax , esi

Comparação em Meld:


Mais uma vez, o número de instruções é o mesmo, assim como (quase) as próprias instruções. A única diferença é a ordem de inicialização das variáveis ​​e a condição de verificação para a continuação do ciclo. Você pode observar que, no cálculo da soma, apenas o primeiro comprimento da matriz é levado em consideração. É óbvio que isso:
int arrayLength2 = localRefToArray . Comprimento ;
mov edi , dword ptr [ edx + 4 ]
if ( i> = arrayLength2 ) lança novo IndexOutOfRangeException ( ) ;
cmp ecx edi
jae 04b12d39

em todos os quatro métodos, há uma matriz inline que limita a verificação e é executada para cada elemento da matriz.

Já podemos chegar à primeira conclusão: usar uma variável extra para tentar acelerar o ciclo é uma perda de tempo, pois o compilador fará isso por você de qualquer maneira. O único motivo para armazenar uma matriz de comprimento em uma variável é tornar o código mais legível.

ForEach é outra situação completamente. Considere os três métodos a seguir:

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

E aqui está o código após o JIT:

ForEachWithoutLength ()
int soma = 0;
xor esi , esi
int [] localRefToArray = this.array;
mov ecx , dword ptr [ ecx + 4 ]
int = 0;
xor edx , edx
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ ecx + 4 ]
; if (arrayLength == 0) goto exit;
teste edi edi
saída jle
; int t = matriz [i];
loop :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
soma + = i;
adicione esi , eax
; i ++;
inc edx
; if (i <arrayLength) goto loop
cmp edi edx
jg loop
soma de retorno;
saída :
mov eax , esi

ForEachWithLengthWithoutLocalVariable ()
int soma = 0;
xor esi , esi
int [] localRefToArray = this.array;
mov ecx , dword ptr [ ecx + 4 ]
int = 0;
xor edx , edx
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ ecx + 4 ]
; if (arrayLength == 0) sair
teste edi edi
saída jle
; int t = matriz [i];
loop :
mov eax , dword ptr [ ecx + edx * 4 + 8 ]
soma + = i;
adicione esi , eax
; sum + = localRefToArray.Length;
adicione esi , dword ptr [ ecx + 4 ]
; i ++;
inc edx
; if (i <arrayLength) goto loop
cmp edi edx
jg loop
soma de retorno;
saída :
mov eax , esi

ForEachWithLengthWithLocalVariable ()
int soma = 0;
xor esi , esi
int [] localRefToArray = this.array;
mov edx , dword ptr [ ecx + 4 ]
int comprimento = localRefToArray.Length;
mov ebx , dword ptr [ edx + 4 ]
int = 0;
xor ecx , ecx
; int arrayLength = localRefToArray.Length;
mov edi , dword ptr [ edx + 4 ]
; if (arrayLength == 0) goto exit;
teste edi edi
saída jle
; int t = matriz [i];
loop :
mov eax , dword ptr [ edx + ecx * 4 + 8 ]
soma + = i;
adicione esi , eax
soma + = comprimento;
adicione esi , ebx
; i ++;
inc ecx
; if (i <arrayLength) goto loop
cmp edi ecx
jg loop
soma de retorno;
saída :
mov eax , esi

A primeira coisa que vem à mente é que são necessárias menos instruções do assembler que o ciclo for (por exemplo, para somar elementos simples, foram necessárias 12 instruções no foreach , mas 15 no for ).
Comparação


No geral, aqui estão os resultados do benchmark for vs foreach para matrizes de 1 milhão de elementos:
 sum+=array[i]; 
Método
ItemsCount
Mean
Erro
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,558 ms
0,3204 ms
0,9447 ms
1.740 ms
1,23
0,65
E para
 sum+=array[i] + array.Length; 
Método
ItemsCount
Mean
Erro
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

O ForEach percorre a matriz muito mais rapidamente do que para . Porque Para descobrir, precisamos comparar o código após o JIT:

Comparação das três opções foreach


Vejamos ForEachWithoutLength. O comprimento da matriz é solicitado apenas uma vez e não há verificações para os limites da matriz. Isso acontece porque o ciclo ForEach primeiro restringe a alteração da coleção dentro do ciclo e o segundo nunca sai da coleção. Por isso, o JIT pode se dar ao luxo de remover os limites da matriz de cheques.

Agora, vamos examinar cuidadosamente ForEachWithLengthWIthoutLocalVariable. Há apenas uma parte estranha, em que soma + = length não acontece com a variável local arrayLength salva anteriormente, mas com uma nova que o aplicativo solicita da memória a cada vez. Isso significa que haverá solicitações de memória N + 1 para o comprimento da matriz, em que N é um comprimento da matriz.

E agora chegamos ao ForEachWithLengthWithLocalVariable. O código é exatamente o mesmo que no exemplo anterior, exceto a manipulação do comprimento da matriz. O compilador mais uma vez gerou uma variável local arrayLength que é usada para verificar se a matriz está vazia, mas o compilador ainda salvou honestamente nosso comprimento de variável local declarado, e é isso que é usado na soma dentro do ciclo. Acontece que esse método solicita o comprimento da matriz da memória apenas duas vezes. A diferença é muito difícil de notar no mundo real.

Em todos os casos, o código do assembler ficou tão simples porque os métodos são simples. Se os métodos tivessem mais parâmetros, ele teria que trabalhar com a pilha, as variáveis ​​poderiam obter lojas fora dos registradores, haveria mais verificações, mas a lógica principal permaneceria a mesma: introduzir uma variável local para o comprimento da matriz é apenas útil para criar código mais legível. Também ocorreu que o Foreach geralmente percorre a matriz mais rapidamente que o For .

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


All Articles