Diferença de desempenho para estruturas de controle 'for' e 'foreach' em C #

105

Qual trecho de código terá melhor desempenho? Os segmentos de código abaixo foram escritos em C #.

1

for(int counter=0; counter<list.Count; counter++)
{
    list[counter].DoSomething();
}

2

foreach(MyType current in list)
{
    current.DoSomething();
}
Kthevar
fonte
31
Eu imagino que isso realmente não importa. Se você está tendo problemas de desempenho, quase certamente não é devido a isso. Não que você não deva fazer a pergunta ...
darasd
2
A menos que seu aplicativo seja muito crítico em termos de desempenho, eu não me preocuparia com isso. É muito melhor ter um código limpo e de fácil compreensão.
Fortyrunner
2
Me preocupa que algumas das respostas aqui pareçam ter sido postadas por pessoas que simplesmente não têm o conceito de um iterador em nenhum lugar de seu cérebro e, portanto, nenhum conceito de enumeradores ou ponteiros.
Ed James
3
Esse segundo código não compilará. System.Object não tem nenhum membro chamado 'valor' (a menos que você seja realmente perverso, o definiu como um método de extensão e está comparando delegados). Digite fortemente seu foreach.
Trillian
1
O primeiro código também não compilará, a menos que o tipo de listrealmente tenha um countmembro em vez de Count.
Jon Skeet

Respostas:

130

Bem, isso depende parcialmente do tipo exato de list. Também dependerá do CLR exato que você está usando.

Se é de alguma forma significativo ou não, dependerá se você está fazendo algum trabalho real no loop. Em quase todos os casos, a diferença no desempenho não será significativa, mas a diferença na legibilidade favorece o foreachloop.

Eu pessoalmente usaria o LINQ para evitar o "se" também:

foreach (var item in list.Where(condition))
{
}

EDIT: Para aqueles de vocês que afirmam que iterar em um List<T>com foreachproduz o mesmo código que o forloop, aqui está a evidência de que não:

static void IterateOverList(List<object> list)
{
    foreach (object o in list)
    {
        Console.WriteLine(o);
    }
}

Produz IL de:

.method private hidebysig static void  IterateOverList(class [mscorlib]System.Collections.Generic.List`1<object> list) cil managed
{
  // Code size       49 (0x31)
  .maxstack  1
  .locals init (object V_0,
           valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object> V_1)
  IL_0000:  ldarg.0
  IL_0001:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<object>::GetEnumerator()
  IL_0006:  stloc.1
  .try
  {
    IL_0007:  br.s       IL_0017
    IL_0009:  ldloca.s   V_1
    IL_000b:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::get_Current()
    IL_0010:  stloc.0
    IL_0011:  ldloc.0
    IL_0012:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_0017:  ldloca.s   V_1
    IL_0019:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::MoveNext()
    IL_001e:  brtrue.s   IL_0009
    IL_0020:  leave.s    IL_0030
  }  // end .try
  finally
  {
    IL_0022:  ldloca.s   V_1
    IL_0024:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>
    IL_002a:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_002f:  endfinally
  }  // end handler
  IL_0030:  ret
} // end of method Test::IterateOverList

O compilador trata os arrays de maneira diferente, convertendo um foreachloop basicamente em um forloop, mas não List<T>. Este é o código equivalente para uma matriz:

static void IterateOverArray(object[] array)
{
    foreach (object o in array)
    {
        Console.WriteLine(o);
    }
}

// Compiles into...

.method private hidebysig static void  IterateOverArray(object[] 'array') cil managed
{
  // Code size       27 (0x1b)
  .maxstack  2
  .locals init (object V_0,
           object[] V_1,
           int32 V_2)
  IL_0000:  ldarg.0
  IL_0001:  stloc.1
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.2
  IL_0004:  br.s       IL_0014
  IL_0006:  ldloc.1
  IL_0007:  ldloc.2
  IL_0008:  ldelem.ref
  IL_0009:  stloc.0
  IL_000a:  ldloc.0
  IL_000b:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0010:  ldloc.2
  IL_0011:  ldc.i4.1
  IL_0012:  add
  IL_0013:  stloc.2
  IL_0014:  ldloc.2
  IL_0015:  ldloc.1
  IL_0016:  ldlen
  IL_0017:  conv.i4
  IL_0018:  blt.s      IL_0006
  IL_001a:  ret
} // end of method Test::IterateOverArray

Curiosamente, não consigo encontrar isso documentado na especificação C # 3 em nenhum lugar ...

Jon Skeet
fonte
Por curiosidade, Jon, o cenário com List <T> acima ... isso também se aplica a outras coleções? Além disso, como você sabia disso (sem intenção de malícia de qualquer espécie) ... como em .. você literalmente tropeçou nisso enquanto tentava responder a esta pergunta, algum tempo atrás? É tão ... aleatório / secreto :)
Pure.Krome
5
Estou ciente das otimizações de array há algum tempo - arrays são um tipo de coleção "central"; o compilador C # já está profundamente ciente deles, então faz sentido tratá-los de maneira diferente. O compilador não tem (e não deveria) ter nenhum conhecimento especial sobre List<T>.
Jon Skeet
Saúde :) e sim ... array's foram o primeiro conceito de coleção que me ensinaram anos e anos atrás na universidade .. então faria sentido que o compilador é inteligente o suficiente para lidar com um dos (se não o) tipo mais primitivo de coleção. aplausos de novo!
Pure.Krome
3
@JonSkeet A otimização do iterador da lista muda o comportamento quando a lista é modificada durante a iteração. Você perde exceção se modificado. Ainda é possível otimizar, mas requer a verificação de que nenhuma modificação aconteça (incluindo em outros threads, eu suponho).
Craig Gidney
5
@VeeKeyBee: Foi o que disse a Microsoft em 2004. a) as coisas mudam; b) o trabalho teria que realizar pequenas quantidades de trabalho em cada iteração para que isso fosse significativo. Observe que foreachsobre uma matriz é equivalente a forqualquer maneira. Sempre codifique primeiro para legibilidade e, em seguida, somente micro-otimize quando tiver evidências de que isso oferece um benefício de desempenho mensurável.
Jon Skeet
15

Um forloop é compilado para um código aproximadamente equivalente a este:

int tempCount = 0;
while (tempCount < list.Count)
{
    if (list[tempCount].value == value)
    {
        // Do something
    }
    tempCount++;
}

Onde como um foreachloop é compilado para um código aproximadamente equivalente a este:

using (IEnumerator<T> e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        T o = (MyClass)e.Current;
        if (row.value == value)
        {
            // Do something
        }
    }
}

Como você pode ver, tudo dependeria de como o enumerador é implementado versus como o indexador de listas é implementado. Acontece que o enumerador para tipos baseados em matrizes normalmente é escrito da seguinte forma:

private static IEnumerable<T> MyEnum(List<T> list)
{
    for (int i = 0; i < list.Count; i++)
    {
        yield return list[i];
    }
}

Como você pode ver, neste caso, não fará muita diferença, no entanto, o enumerador de uma lista vinculada provavelmente seria algo assim:

private static IEnumerable<T> MyEnum(LinkedList<T> list)
{
    LinkedListNode<T> current = list.First;
    do
    {
        yield return current.Value;
        current = current.Next;
    }
    while (current != null);
}

No .NET, você descobrirá que a classe LinkedList <T> nem mesmo tem um indexador, portanto, você não seria capaz de fazer seu loop for em uma lista vinculada; mas se você pudesse, o indexador teria que ser escrito assim:

public T this[int index]
{
       LinkedListNode<T> current = this.First;
       for (int i = 1; i <= index; i++)
       {
            current = current.Next;
       }
       return current.value;
}

Como você pode ver, chamar isso várias vezes em um loop será muito mais lento do que usar um enumerador que se lembre de onde ele está na lista.

Martin Brown
fonte
12

Um teste fácil de semivalidar. Fiz um pequeno teste, só para ver. Aqui está o código:

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    for (int i = 0; i < 10000000; i++)
    {
        intList.Add(i);
    }

    DateTime timeStarted = DateTime.Now;
    for (int i = 0; i < intList.Count; i++)
    {
        int foo = intList[i] * 2;
        if (foo % 2 == 0)
        {
        }
    }

    TimeSpan finished = DateTime.Now - timeStarted;

    Console.WriteLine(finished.TotalMilliseconds.ToString());
    Console.Read();

}

E aqui está a seção foreach:

foreach (int i in intList)
{
    int foo = i * 2;
    if (foo % 2 == 0)
    {
    }
}

Quando substituí o for por um foreach - o foreach foi 20 milissegundos mais rápido - de forma consistente . O for foi de 135-139ms, enquanto o foreach foi de 113-119ms. Troquei de um lado para o outro várias vezes, certificando-me de que não era algum processo que apenas começou.

No entanto, quando removi o foo e a instrução if, o for foi 30 ms mais rápido (foreach foi de 88 ms e for foi de 59 ms). Ambos eram conchas vazias. Estou assumindo que o foreach realmente passou uma variável onde o for estava apenas incrementando uma variável. Se eu adicionar

int foo = intList[i];

Em seguida, a velocidade de for ficou lenta em cerca de 30ms. Estou assumindo que isso teve a ver com a criação de foo e pegando a variável na matriz e atribuindo-a a foo. Se você acessar apenas intList [i], você não terá essa penalidade.

Com toda a honestidade ... Eu esperava que o foreach fosse um pouco mais lento em todas as circunstâncias, mas não o suficiente para importar na maioria das aplicações.

editar: aqui está o novo código usando as sugestões de Jons (134217728 é o maior int que você pode ter antes que a exceção System.OutOfMemory seja lançada):

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    Console.WriteLine("Generating data.");
    for (int i = 0; i < 134217728 ; i++)
    {
        intList.Add(i);
    }

    Console.Write("Calculating for loop:\t\t");

    Stopwatch time = new Stopwatch();
    time.Start();
    for (int i = 0; i < intList.Count; i++)
    {
        int foo = intList[i] * 2;
        if (foo % 2 == 0)
        {
        }
    }

    time.Stop();
    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
    Console.Write("Calculating foreach loop:\t");
    time.Reset();
    time.Start();

    foreach (int i in intList)
    {
        int foo = i * 2;
        if (foo % 2 == 0)
        {
        }
    }

    time.Stop();

    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
    Console.Read();
}

E aqui estão os resultados:

Gerando dados. Calculando o loop: 2458ms Calculando o loop foreach: 2005ms

Trocá-los para ver se lida com a ordem das coisas produz os mesmos resultados (quase).

Kenny Mann
fonte
6
É melhor usar o cronômetro do que DateTime.Now - e eu não confiaria em nenhuma corrida tão rápida, para ser honesto.
Jon Skeet
8
Seus loops foreach estão rodando mais rápido porque um 'for' avalia a condição de cada iteração. No caso do seu exemplo, isso cria uma chamada de método extra (para obter list.count). Em suma, você está comparando duas partes diferentes de código, daí seus resultados estranhos. Tente 'int max = intlist.Count; for (int i = 0; i <max; i ++) ... 'e o loop' for 'sempre será executado mais rápido, como esperado!
AR
1
Após a compilação, for e foreach otimizam exatamente a mesma coisa ao trabalhar com primitivos. Não é até que você introduza List <T> que eles diferem (muito) em velocidade.
Anthony Russell
9

Observação: essa resposta se aplica mais ao Java do que ao C #, já que o C # não tem um indexador ativado LinkedLists, mas acho que o ponto geral ainda é válido.

Se o com o qual listvocê está trabalhando for um LinkedList, o desempenho do código do indexador ( acesso de estilo de matriz ) é muito pior do que usar o IEnumeratorde foreach, para listas grandes.

Quando você acessa o elemento 10.000 em a LinkedListusando a sintaxe do indexador :, list[10000]a lista encadeada começará no nó principal e percorrerá o Next-pointer dez mil vezes, até atingir o objeto correto. Obviamente, se você fizer isso em um loop, obterá:

list[0]; // head
list[1]; // head.Next
list[2]; // head.Next.Next
// etc.

Quando você chama GetEnumerator(usando implicitamente a forachsintaxe -sint), você obterá umIEnumerator objeto que tem um ponteiro para o nó principal. Cada vez que você chama MoveNext, esse ponteiro é movido para o próximo nó, assim:

IEnumerator em = list.GetEnumerator();  // Current points at head
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
// etc.

Como você pode ver, no caso de LinkedList s, o método do indexador de array se torna cada vez mais lento, quanto mais tempo você faz o loop (ele tem que passar pelo mesmo ponteiro de cabeça repetidamente). Já o IEnumerablejusto opera em tempo constante.

Claro, como Jon disse, isso realmente depende do tipo de list, se listnão for um LinkedList, mas um array, o comportamento é completamente diferente.

Tom Lokhorst
fonte
4
LinkedList no .NET não tem um indexador, portanto, não é realmente uma opção.
Jon Skeet
Bem, isso resolve o problema, então :-) Estou apenas olhando os LinkedList<T>documentos no MSDN e tem uma API bastante decente. Mais importante ainda, não tem um get(int index)método, como o Java. Ainda assim, acho que o ponto ainda é válido para qualquer outra estrutura de dados do tipo lista que exponha um indexador mais lento do que um específico IEnumerator.
Tom Lokhorst
2

Como outras pessoas mencionaram, embora o desempenho não importe muito, o foreach sempre será um pouco mais lento por causa do uso de IEnumerable/ IEnumeratorno loop. O compilador traduz a construção em chamadas nessa interface e, para cada etapa, uma função + uma propriedade é chamada na construção foreach.

IEnumerator iterator = ((IEnumerable)list).GetEnumerator();
while (iterator.MoveNext()) {
  var item = iterator.Current;
  // do stuff
}

Esta é a expansão equivalente da construção em C #. Você pode imaginar como o impacto no desempenho pode variar com base nas implementações de MoveNext e Current. Considerando que em um acesso de array, você não tem essas dependências.

Charles Prakash Dasari
fonte
4
Não se esqueça de que há uma diferença entre um acesso de array e um acesso de indexador. Se a lista for um List<T>aqui, então ainda há o hit (possivelmente inline) de chamar o indexador. Não é como se fosse um acesso de array bare metal.
Jon Skeet
Muito verdadeiro! É mais uma execução imobiliária e estamos à mercê da implementação.
Charles Prakash Dasari
1

Depois de ler argumentos suficientes para que "o loop foreach seja preferido para facilitar a leitura", posso dizer que minha primeira reação foi "o quê"? A legibilidade, em geral, é subjetiva e, neste caso particular, ainda mais. Para alguém com experiência em programação (praticamente todas as linguagens anteriores ao Java), os loops for são muito mais fáceis de ler do que os loops foreach. Além disso, as mesmas pessoas que afirmam que os loops foreach são mais legíveis também são defensores do linq e de outros "recursos" que tornam o código difícil de ler e manter, algo que prova o ponto acima.

Sobre o impacto no desempenho, veja a resposta a esta pergunta.

EDIT: Existem coleções em C # (como o HashSet) que não possuem indexador. Nessas coleções, foreach é a única forma de interagir e é o único caso que eu acho que deveria ser utilizado ao longo de .

ThunderGr
fonte
0

Há um outro fato interessante que pode ser facilmente esquecido ao testar a velocidade de ambos os loops: usar o modo de depuração não permite que o compilador otimize o código usando as configurações padrão.

Isso me levou ao resultado interessante de que foreach é mais rápido do que no modo de depuração. Enquanto o for é mais rápido do que o foreach no modo de liberação. Obviamente, o compilador tem maneiras melhores de otimizar um loop for do que um loop foreach, que compromete várias chamadas de método. Um loop for é, aliás, tão fundamental que é possível que seja até otimizado pela própria CPU.

Sam
fonte
0

No exemplo que você forneceu, é definitivamente melhor usar foreachloop em vez de forloop.

A foreachconstrução padrão pode ser mais rápida (1,5 ciclos por etapa) do que uma simples for-loop(2 ciclos por etapa), a menos que o loop tenha sido desenrolado (1,0 ciclo por etapa).

Assim, por código de todos os dias, o desempenho não é uma razão para usar as mais complexas for, whileou do-whileconstruções.

Confira este link: http://www.codeproject.com/Articles/146797/Fast-and-Less-Fast-Loops-in-C


╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗
        Method         List<int>  int[]  Ilist<int> onList<Int>  Ilist<int> on int[] 
╠══════════════════════╬═══════════╬═══════╬════════════════════════╬═════════════════════╣
 Time (ms)             23,80      17,56  92,33                   86,90               
 Transfer rate (GB/s)  2,82       3,82   0,73                    0,77                
 % Max                 25,2%      34,1%  6,5%                    6,9%                
 Cycles / read         3,97       2,93   15,41                   14,50               
 Reads / iteration     16         16     16                      16                  
 Cycles / iteration    63,5       46,9   246,5                   232,0               
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝

rpax
fonte
4
Você pode reler o artigo do projeto de código que você vinculou. É um artigo interessante, mas diz exatamente o oposto de sua postagem. Além disso, a tabela que você recriou está medindo o desempenho de acessar um array e List diretamente ou por meio de suas interfaces IList. Nem tem nada a ver com a pergunta. :)
Paul Walls
0

você pode ler sobre isso no Deep .NET - parte 1 Iteração

ele cobre os resultados (sem a primeira inicialização) do código-fonte .NET até a desmontagem.

por exemplo - Iteração de matriz com um loop foreach: insira a descrição da imagem aqui

e - iteração de lista com loop foreach: insira a descrição da imagem aqui

e os resultados finais: insira a descrição da imagem aqui

insira a descrição da imagem aqui

Ou Yaacov
fonte