Database
 sql >> Base de Dados >  >> RDS >> Database

Foreach ou For – eis a questão


A discussão sobre a diferença de preferência entre FOREACH e FOR não é nova. Todos sabemos que o FOREACH é mais lento, mas nem todos sabem o porquê.

Quando comecei a aprender .NET, uma pessoa me disse que o FOREACH é duas vezes mais lento que o FOR. Ele disse isso sem qualquer fundamento. Eu tomei isso como certo.

Eventualmente, decidi explorar a diferença de desempenho do loop FOREACH e FOR e escrever este artigo para discutir as nuances.
Vamos dar uma olhada no código a seguir:
foreach (var item in Enumerable.Range(0, 128))
{
  Console.WriteLine(item);
}

O FOREACH é um açúcar de sintaxe. Nesse caso específico, o compilador o transforma no seguinte código:
IEnumerator<int> enumerator = Enumerable.Range(0, 128).GetEnumerator();
try
 {
   while (enumerator.MoveNext())
   {
     int item = enumerator.Current;
     Console.WriteLine(item);
   }
 }
finally
 {
  if (enumerator != null)
  {
   enumerator.Dispose();
  }
}

Sabendo disso, podemos supor o motivo pelo qual o FOREACH é mais lento que o FOR:
  • Um novo objeto está sendo criado. Chama-se Criador.
  • O método MoveNext é chamado em cada iteração.
  • Cada iteração acessa a propriedade Current.

É isso! No entanto, nem tudo é tão fácil quanto parece.

Felizmente (ou infelizmente), C#/CLR pode realizar otimizações em tempo de execução. A vantagem é que o código funciona mais rápido. O contra – os desenvolvedores devem estar cientes dessas otimizações.

O array é um tipo profundamente integrado ao CLR, e o CLR fornece várias otimizações para esse tipo. O loop FOREACH é uma entidade iterável, que é um aspecto chave do desempenho. Mais adiante neste artigo, discutiremos como iterar por meio de arrays e listas com a ajuda do método estático Array.ForEach e do método List.ForEach.

Métodos de teste

static double ArrayForWithoutOptimization(int[] array)
{
   int sum = 0;
   var watch = Stopwatch.StartNew();
   for (int i = 0; i < array.Length; i++)
     sum += array[i];
    watch.Stop();
    return watch.Elapsed.TotalMilliseconds;
}

static double ArrayForWithOptimization(int[] array)
{
   int length = array.Length;
   int sum = 0;
   var watch = Stopwatch.StartNew();
    for (int i = 0; i < length; i++)
      sum += array[i];
    watch.Stop();
     return watch.Elapsed.TotalMilliseconds;
}

static double ArrayForeach(int[] array)
{
  int sum = 0;
  var watch = Stopwatch.StartNew();
   foreach (var item in array)
    sum += item;
  watch.Stop();
  return watch.Elapsed.TotalMilliseconds;
}

static double ArrayForEach(int[] array)
{
  int sum = 0;
  var watch = Stopwatch.StartNew();
  Array.ForEach(array, i => { sum += i; });
  watch.Stop();
  return watch.Elapsed.TotalMilliseconds;
}

Condições de teste:
  • A opção "Otimizar código" está ativada.
  • O número de elementos é igual a 100.000.000 (tanto na matriz quanto na lista).
  • Especificação do PC:Intel Core i-5 e 8 GB de RAM.

Matrizes


O diagrama mostra que FOR e FOREACH gastam a mesma quantidade de tempo enquanto iteram por arrays. E é porque a otimização CLR converte FOREACH em FOR e usa o comprimento da matriz como o limite máximo de iteração. Não importa se o comprimento do array é armazenado em cache ou não (ao usar FOR), o resultado é quase o mesmo.

Pode parecer estranho, mas armazenar em cache o comprimento do array pode afetar o desempenho. Ao usar matriz .Length como o limite de iteração, JIT testa o índice para atingir a borda direita além do ciclo. Esta verificação é realizada apenas uma vez.
É muito fácil destruir essa otimização. O caso em que a variável é armazenada em cache dificilmente é otimizado.

Array.foreach apresentou os piores resultados. Sua implementação é bastante simples:
public static void ForEach<T>(T[] array, Action<T> action)
 {
  for (int index = 0; index < array.Length; ++index)
    action(array[index]);
 }

Então por que está rodando tão devagar? Ele usa FOR sob o capô. Bem, o motivo está em chamar o delegado ACTION. Na verdade, um método é chamado em cada iteração, o que diminui o desempenho. Além disso, os delegados são invocados não tão rápido quanto gostaríamos.

Listas


O resultado é completamente diferente. Ao iterar listas, FOR e FOREACH mostram resultados diferentes. Não há otimização. FOR (com cache do comprimento da lista) mostra o melhor resultado, enquanto FOREACH é mais de 2 vezes mais lento. É porque lida com MoveNext e Current sob o capô. List.ForEach assim como Array.ForEach mostra o pior resultado. Os delegados são sempre chamados virtualmente. A implementação deste método fica assim:
public void ForEach(Action<T> action)
{
  int num = this._version;
   for (int index = 0; index < this._size && num == this._version; ++index)
     action(this._items[index]);
   if (num == this._version)
     return;
   ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

Cada iteração chama o delegado de ação. Ele também verifica se a lista foi alterada e, em caso afirmativo, uma exceção é lançada.

List usa internamente um modelo baseado em array e o método ForEach usa o índice de array para iterar, o que é significativamente mais rápido do que usar o indexador.

Números específicos

  1. O loop FOR sem cache de comprimento e o FOREACH funcionam um pouco mais rápido em arrays do que FOR com cache de comprimento.
  2. Matriz.Foreach desempenho é aproximadamente 6 vezes mais lento que o desempenho FOR / FOREACH.
  3. O loop FOR sem cache de comprimento funciona 3 vezes mais devagar em listas, em comparação com arrays.
  4. O loop FOR com cache de comprimento funciona 2 vezes mais devagar em listas, em comparação com arrays.
  5. O loop FOREACH funciona 6 vezes mais devagar em listas, em comparação com arrays.

Aqui está um quadro de líderes para listas:

E para matrizes:

Conclusão


Eu realmente gostei desta investigação, especialmente o processo de escrita, e espero que você tenha gostado também. Como se viu, FOREACH é mais rápido em arrays do que FOR com busca de comprimento. Em estruturas de lista, FOREACH é mais lento que FOR.

O código fica melhor ao usar o FOREACH, e os processadores modernos permitem usá-lo. No entanto, se você precisar otimizar muito sua base de código, é melhor usar FOR.

O que você acha, qual loop é mais rápido, FOR ou FOREACH?