Differenza di prestazioni per le strutture di controllo "for" e "foreach" in C #


105

Quale snippet di codice darà prestazioni migliori? I segmenti di codice seguenti sono stati scritti in C #.

1.

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

2.

foreach(MyType current in list)
{
    current.DoSomething();
}

31
Immagino che non importi davvero. Se hai problemi di prestazioni, quasi certamente non è dovuto a questo. Non che tu non debba fare la domanda ...
darasd

2
A meno che la tua app non sia molto critica per le prestazioni, non mi preoccuperei di questo. Molto meglio avere un codice pulito e facilmente comprensibile.
Fortyrunner

2
Mi preoccupa che alcune delle risposte qui sopra sembrano essere pubblicate da persone che semplicemente non hanno il concetto di un iteratore da nessuna parte nel loro cervello, e quindi nessun concetto di enumeratori o puntatori.
Ed James,

3
Il secondo codice non verrà compilato. System.Object non ha alcun membro chiamato 'valore' (a meno che tu non sia davvero malvagio, lo abbia definito come un metodo di estensione e stia confrontando i delegati). Digita fortemente il tuo foreach.
Trillian

1
Neanche il primo codice verrà compilato, a meno che il tipo di listnon abbia davvero un countmembro al posto di Count.
Jon Skeet

Risposte:


130

Bene, in parte dipende dal tipo esatto di list. Dipenderà anche dall'esatto CLR che stai utilizzando.

Che sia in qualche modo significativo o meno dipenderà dal fatto che tu stia facendo un lavoro reale nel ciclo. In quasi tutti i casi, la differenza di prestazioni non sarà significativa, ma la differenza di leggibilità favorisce il foreachloop.

Personalmente userei LINQ per evitare anche il "se":

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

EDIT: Per quelli di voi che affermano che l'iterazione su un List<T>with foreachproduce lo stesso codice del forciclo, ecco le prove che non lo fa:

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

Produce IL di:

.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

Il compilatore tratta gli array in modo diverso, convertendo un foreachciclo fondamentalmente in un forciclo, ma non List<T>. Ecco il codice equivalente per un array:

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

È interessante notare che non riesco a trovare questo documentato nelle specifiche C # 3 da nessuna parte ...


Per interesse Jon, lo scenario con List <T> sopra ... vale anche per altre raccolte? Inoltre, come facevi a saperlo (senza alcuna intenzione di malizia) ... come in ... sei letteralmente inciampato in questo mentre cercavi di rispondere a questa domanda, in precedenza qualche tempo fa? È così ... casuale / segreto :)
Pure.Krome

5
Sono stato a conoscenza delle ottimizzazioni degli array per un po '- gli array sono un tipo di raccolta "centrale"; il compilatore C # ne è già profondamente consapevole, quindi ha senso trattarli in modo diverso. Il compilatore non (e non dovrebbe) avere alcuna conoscenza speciale di List<T>.
Jon Skeet

Ciao :) e sì ... gli array sono stati il ​​primo concetto di raccolta che mi è stato insegnato anni e anni fa all'università .. quindi sarebbe logico che il compilatore sia abbastanza intelligente da gestire uno dei (se non il) tipo più primitivo di collezione. applausi ancora!
Pure.Krome

3
@JonSkeet L'ottimizzazione dell'iteratore della lista cambia il comportamento quando la lista viene modificata durante l'iterazione. Perdi l'eccezione se modificata. È ancora possibile ottimizzare, ma è necessario verificare che non avvengano modifiche (anche su altri thread, presumo).
Craig Gidney

5
@VeeKeyBee: Così ha detto Microsoft nel 2004. a) le cose cambiano; b) il lavoro dovrebbe essere svolto in piccole quantità di lavoro su ogni iterazione affinché questo sia significativo. Notare che foreachover un array equivale a forcomunque. Sempre il codice per migliorare la leggibilità, poi solo micro-ottimizzare quando si ha la prova che dà un miglioramento delle prestazioni misurabili.
Jon Skeet

15

Un forciclo viene compilato in un codice approssimativamente equivalente a questo:

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

Dove come un foreachciclo viene compilato in un codice approssimativamente equivalente a questo:

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

Quindi, come puoi vedere, tutto dipenderà da come viene implementato l'enumeratore rispetto a come viene implementato l'indicizzatore delle liste. A quanto pare, l'enumeratore per i tipi basati su array è normalmente scritto in questo modo:

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

Quindi, come puoi vedere, in questo caso non farà molta differenza, tuttavia l'enumeratore per un elenco collegato sarebbe probabilmente simile a questo:

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

Nel .NET scoprirai che la classe LinkedList <T> non ha nemmeno un indicizzatore, quindi non saresti in grado di fare il tuo ciclo for su una lista collegata; ma se potessi, l'indicizzatore dovrebbe essere scritto in questo modo:

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

Come puoi vedere, chiamarlo più volte in un ciclo sarà molto più lento rispetto all'utilizzo di un enumeratore in grado di ricordare dove si trova nell'elenco.


12

Un test facile da semi-convalidare. Ho fatto un piccolo test, solo per vedere. Ecco il codice:

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();

}

Ed ecco la sezione foreach:

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

Quando ho sostituito il for con un foreach, il foreach era più veloce di 20 millisecondi, in modo coerente . Il for era di 135-139 ms mentre il foreach era di 113-119 ms. Ho scambiato avanti e indietro diverse volte, assicurandomi che non fosse un processo che si è semplicemente attivato.

Tuttavia, quando ho rimosso il foo e l'istruzione if, for era più veloce di 30 ms (foreach era 88ms e for era 59ms). Erano entrambi gusci vuoti. Suppongo che foreach abbia effettivamente passato una variabile dove come for stava solo incrementando una variabile. Se ho aggiunto

int foo = intList[i];

Quindi il per diventa lento di circa 30 ms. Presumo che questo avesse a che fare con la creazione di foo e il recupero della variabile nell'array e l'assegnazione a foo. Se accedi a intList [i], non hai quella penalità.

In tutta onestà .. mi aspettavo che foreach fosse leggermente più lento in tutte le circostanze, ma non abbastanza da essere importante nella maggior parte delle applicazioni.

modifica: ecco il nuovo codice utilizzando i suggerimenti di Jons (134217728 è il più grande int che puoi avere prima che venga generata l'eccezione System.OutOfMemory):

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

Ed ecco i risultati:

Generazione di dati. Calcolo per loop: 2458 ms Calcolo per loop foreach: 2005 ms

Scambiandoli per vedere se si tratta dell'ordine delle cose produce gli stessi risultati (quasi).


6
È meglio usare Stopwatch piuttosto che DateTime.Ora - e non mi fiderei di nessuna corsa così veloce, ad essere onesti.
Jon Skeet

8
I tuoi cicli foreach vengono eseguiti più velocemente perché un 'for' valuta la condizione a ogni iterazione. Nel caso del tuo esempio, questo fa per una chiamata di metodo extra (per ottenere list.count) In breve, stai confrontando due diversi pezzi di codice, da qui i tuoi strani risultati. Prova 'int max = intlist.Count; for (int i = 0; i <max; i ++) ... 'e il ciclo' for 'verrà sempre eseguito più velocemente, come previsto!
AR

1
Dopo la compilazione, for e foreach si ottimizzano esattamente allo stesso modo quando si lavora con le primitive. È solo quando si introduce List <T> che differiscono (notevolmente) in velocità.
Anthony Russell,

9

Nota: questa risposta si applica più a Java che a C #, poiché C # non ha un indicizzatore attivo LinkedLists, ma penso che il punto generale valga ancora.

Se liststai lavorando con a LinkedList, le prestazioni del codice indicizzatore ( accesso in stile array ) sono molto peggiori rispetto all'utilizzo di IEnumeratorda foreach, per elenchi di grandi dimensioni.

Quando si accede all'elemento 10.000 in a LinkedListutilizzando la sintassi dell'indicizzatore:, list[10000]l'elenco collegato inizierà dal nodo head e attraverserà il Nextpuntatore diecimila volte, fino a raggiungere l'oggetto corretto. Ovviamente, se lo fai in un ciclo, otterrai:

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

Quando si chiama GetEnumerator(implicitamente utilizzando il forach-syntax), si otterrà un IEnumeratoroggetto che ha un puntatore al nodo principale. Ogni volta che chiami MoveNext, quel puntatore viene spostato al nodo successivo, in questo modo:

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.

Come puoi vedere, nel caso di LinkedLists, il metodo dell'indicizzatore dell'array diventa sempre più lento, più a lungo si esegue il ciclo (deve passare attraverso lo stesso puntatore a testa più e più volte). Mentre il IEnumerablegiusto opera in tempo costante.

Ovviamente, come ha detto Jon, questo dipende davvero dal tipo di list, se listnon è un LinkedList, ma un array, il comportamento è completamente diverso.


4
LinkedList in .NET non ha un indicizzatore, quindi non è effettivamente un'opzione.
Jon Skeet

Oh, beh, questo risolve il problema, quindi :-) Sto solo guardando i LinkedList<T>documenti su MSDN, e ha un'API abbastanza decente. Ancora più importante, non ha un get(int index)metodo, come fa Java. Tuttavia, immagino che il punto valga ancora per qualsiasi altra struttura di dati simile a un elenco che espone un indicizzatore più lento di uno specifico IEnumerator.
Tom Lokhorst

2

Come altre persone hanno già detto, sebbene le prestazioni in realtà non contino molto, il foreach sarà sempre un po 'più lento a causa dell'utilizzo di IEnumerable/ IEnumeratornel ciclo. Il compilatore traduce il costrutto in chiamate su quell'interfaccia e per ogni passaggio vengono chiamate una funzione + una proprietà nel costrutto foreach.

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

Questa è l'espansione equivalente del costrutto in C #. Puoi immaginare come può variare l'impatto sulle prestazioni in base alle implementazioni di MoveNext e Current. Mentre in un accesso ad array, non hai quelle dipendenze.


4
Non dimenticare che c'è una differenza tra un accesso all'array e un accesso all'indicizzatore. Se la lista è List<T>qui, allora c'è ancora l'hit (possibilmente inline) di chiamare l'indicizzatore. Non è come se fosse un accesso array bare metal.
Jon Skeet

Verissimo! È ancora un'altra esecuzione di proprietà e siamo in balia dell'implementazione.
Charles Prakash Dasari

1

Dopo aver letto abbastanza argomenti che "il ciclo foreach dovrebbe essere preferito per la leggibilità", posso dire che la mia prima reazione è stata "cosa"? La leggibilità, in generale, è soggettiva e, in questo caso particolare, anche di più. Per qualcuno con un background in programmazione (praticamente, ogni linguaggio prima di Java), i cicli for sono molto più facili da leggere rispetto ai cicli foreach. Inoltre, le stesse persone che affermano che i cicli foreach sono più leggibili, sono anche sostenitori di linq e di altre "funzionalità" che rendono il codice difficile da leggere e mantenere, cosa che dimostra quanto sopra.

Per quanto riguarda l'impatto sulle prestazioni, vedere la risposta a questa domanda.

EDIT: ci sono raccolte in C # (come HashSet) che non hanno indicizzatore. In queste collezioni, foreach è l'unico modo per iterare ed è l'unico caso credo che dovrebbe essere utilizzato più per .


0

C'è un altro fatto interessante che può essere facilmente ignorato quando si verifica la velocità di entrambi i loop: l'uso della modalità di debug non consente al compilatore di ottimizzare il codice utilizzando le impostazioni predefinite.

Questo mi ha portato all'interessante risultato che il foreach è più veloce di quello in modalità debug. Mentre il for è più veloce di foreach nella modalità di rilascio. Ovviamente il compilatore ha modi migliori per ottimizzare un ciclo for rispetto a un ciclo foreach che compromette diverse chiamate di metodo. Un ciclo for è talmente fondamentale che è possibile che sia ottimizzato anche dalla CPU stessa.


0

Nell'esempio che hai fornito, è decisamente meglio usare foreachloop invece di un forloop.

Il foreachcostrutto standard può essere più veloce (1,5 cicli per passo) di un semplice for-loop(2 cicli per passo), a meno che il loop non sia stato srotolato (1,0 cicli per passo).

Quindi, per il codice di tutti i giorni, le prestazioni non è una ragione per utilizzare i più complessi for, whileo do-whilecostrutti.

Dai un'occhiata a questo 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               
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝


4
Potresti rileggere l'articolo del progetto sul codice che hai collegato. È un articolo interessante, ma dice l'esatto contrario del tuo post. Inoltre, la tabella che hai ricreato sta misurando le prestazioni di accesso a un array e un List direttamente o tramite le loro interfacce IList. Nessuno dei due ha nulla a che fare con la domanda. :)
Paul Walls

0

puoi leggere a riguardo in Deep .NET - parte 1 Iterazione

copre i risultati (senza la prima inizializzazione) dal codice sorgente .NET fino al disassemblaggio.

ad esempio - Iterazione di array con un ciclo foreach: inserisci qui la descrizione dell'immagine

e - elenca l'iterazione con il ciclo foreach: inserisci qui la descrizione dell'immagine

e il risultato finale: inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.