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
- 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.
- Matriz.Foreach desempenho é aproximadamente 6 vezes mais lento que o desempenho FOR / FOREACH.
- O loop FOR sem cache de comprimento funciona 3 vezes mais devagar em listas, em comparação com arrays.
- O loop FOR com cache de comprimento funciona 2 vezes mais devagar em listas, em comparação com arrays.
- 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?