2009-06-20 9 views
5

Stavo cercando di capire se un ciclo for era più veloce di un ciclo foreach e stavo usando le classi System.Diagnostics per cronometrare il compito. Durante il test ho notato che quale loop mai inserito prima esegue sempre più lentamente l'ultimo. Qualcuno può dirmi perché questo sta accadendo? Il mio codice è qui sotto:Perché il secondo ciclo for esegue sempre più velocemente del primo?

using System; 
using System.Diagnostics; 

namespace cool { 
    class Program { 
     static void Main(string[] args) { 
      int[] x = new int[] { 3, 6, 9, 12 }; 
      int[] y = new int[] { 3, 6, 9, 12 }; 

      DateTime startTime = DateTime.Now; 
      for (int i = 0; i < 4; i++) { 
       Console.WriteLine(x[i]); 
      } 
      TimeSpan elapsedTime = DateTime.Now - startTime; 

      DateTime startTime2 = DateTime.Now; 
      foreach (var item in y) { 
       Console.WriteLine(item); 
      } 
      TimeSpan elapsedTime2 = DateTime.Now - startTime2; 

      Console.WriteLine("\nSummary"); 
      Console.WriteLine("--------------------------\n"); 
      Console.WriteLine("for:\t{0}\nforeach:\t{1}", elapsedTime, elapsedTime2); 

      Console.ReadKey(); 
     } 
    } 
} 

Ecco l'output:

for:   00:00:00.0175781 
foreach:  00:00:00.0009766 
+5

Solo una breve nota: Quando sono i tempi qualcosa per determinare i tempi di esecuzione relativi, no uscita nulla (WriteLine()) all'interno del ciclo. Il tempo necessario per scrivere WriteLine() è probabilmente migliaia (a milioni) di volte più lungo di quello che si sta tentando di testare in modo da perdere ogni senso di accuratezza. Inoltre, avrai bisogno di più di quattro (4) iterazioni per essere significativo. Prova migliaia (o anche milioni). –

+4

C'è solo una risposta reale alla tua domanda: il tuo benchmark è seriamente imperfetto. Altri punti che le persone citano sono vere, ma non è questo il motivo per cui ottieni questo risultato. –

+0

Che cosa dice se esegui prima il ciclo foreach? –

risposta

15

Probabilmente perché le classi (ad esempio console) devono essere JIT-compilato la prima volta attraverso. Otterrai le metriche migliori chiamando prima tutti i metodi (per eseguirli con JIT (warm up)), quindi eseguendo il test.

Come altri utenti hanno indicato, 4 passaggi non saranno mai sufficienti per mostrare la differenza.

Per inciso, la differenza di prestazioni tra for e foreach sarà trascurabile e i vantaggi di leggibilità dell'utilizzo di foreach superano quasi sempre i vantaggi marginali delle prestazioni.

2

Il motivo per cui è che ci sono diverse forme di testa nella versione foreach che non sono presenti nel ciclo for

  • Uso di un IDisposable.
  • Una chiamata di metodo aggiuntiva per ogni elemento. Ogni elemento deve essere accessibile sotto il cofano utilizzando IEnumerator<T>.Current che è una chiamata di metodo. Perché è su un'interfaccia che non può essere in linea. Ciò significa che N chiama il metodo dove N è il numero di elementi nell'enumerazione. Il ciclo for usa e indicizza semplicemente
  • In un ciclo foreach tutte le chiamate passano attraverso un'interfaccia. In generale, questo un po 'più lento rispetto attraverso un tipo concreto

Si prega di notare che le cose che ho elencato sopra sono non necessariamente enormi costi. Sono in genere costi molto piccoli che possono contribuire a una piccola differenza di prestazioni.

Inoltre, come sottolineato da Mehrdad, i compilatori e JIT possono scegliere di ottimizzare un ciclo foreach per alcune strutture di dati noti come un array. Il risultato finale potrebbe essere solo un ciclo for.

Nota: il benchmark delle prestazioni in generale richiede un po 'più di lavoro per essere accurato.

  • È necessario utilizzare uno Stop Watch invece di DateTime. È molto più accurato per i benchmark delle prestazioni.
  • È necessario eseguire il test molte volte non solo una volta
  • È necessario eseguire un'esecuzione fittizia su ciascun ciclo per eliminare i problemi che si verificano con JIT un metodo la prima volta. Questo probabilmente non è un problema quando tutto il codice è nello stesso metodo ma non fa male.
  • È necessario utilizzare più di 4 valori nell'elenco. Prova invece a 40.000.
+0

Mi dispiace non essere stato chiaro. Se metto il ciclo foreach secondo, verrà eseguito più velocemente del ciclo for. –

+0

@JaredPar: il ciclo 'foreach' per gli array è ottimizzato dal compilatore. –

+0

Un altro da aggiungere all'elenco è quello di modificare la priorità del thread su un valore superiore per ridurre al minimo l'interferenza di altri thread in esecuzione sullo stesso processore. – RichardOD

7
  1. Non vorrei usare DateTime per misurare le prestazioni - provare la classe Stopwatch.
  2. Misurare con solo 4 passaggi non ti darà mai un buon risultato. Meglio usare> 100.000 passaggi (è possibile utilizzare un ciclo esterno). Non fare Console.WriteLine nel tuo ciclo.
  3. Ancora meglio: usare un profiler (come Redgate formiche o forse NProf)
+0

Puoi fornire alcuni link su come utilizzare la classe cronometro? –

+4

Classe cronometro (via MSDN): http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.aspx –

+0

Grazie Robert ... – tanascius

1

Si dovrebbe utilizzare lo StopWatch per cronometrare il comportamento.

Tecnicamente, il ciclo per è più veloce. Foreach chiama il metodo MoveNext() (creando uno stack di metodi e altro overhead da una chiamata) sull'iteratore di IEnumerable, quando per deve solo incrementare una variabile.

3

Io non sono così tanto in C#, ma quando ricordo bene, Microsoft stava costruendo compilatori "Just in Time" per Java. Quando usano le stesse o simili tecniche in C#, sarebbe piuttosto naturale che "alcuni costrutti arrivino in secondo luogo più veloci".

Ad esempio, potrebbe essere che il sistema JIT rileva che viene eseguito un ciclo e decide adhoc di compilare l'intero metodo. Quindi, quando viene raggiunto il secondo ciclo, esso è ancora compilato ed esegue molto più velocemente del primo. Ma questa è una mia ipotesi piuttosto semplicistica. Ovviamente è necessario avere una conoscenza molto più approfondita del sistema di runtime C# per capire cosa sta succedendo. Potrebbe anche essere che la RAM-Page viene acceduta per prima nel primo ciclo e nel secondo è ancora nella cache della CPU.

Addon: L'altro commento che è stato fatto: che il modulo di uscita può essere JITed una prima volta nelle prime cuciture ad anello per me più probabile della mia prima ipotesi. Le lingue moderne sono solo molto complesse per scoprire cosa viene fatto sotto il cofano. Anche questa mia affermazione si inserisce in questa ipotesi:

Ma anche i tuoi loop hanno uscite terminali. Rendono le cose ancora più difficili. Potrebbe anche essere, che costa un po 'di tempo per aprire il terminale una prima volta in un programma.

3

Stavo solo eseguendo dei test per ottenere dei numeri reali, ma nel frattempo Gaz mi ha battuto per la risposta: la chiamata a Console.Writeline è azzerata alla prima chiamata, quindi paghi quel costo nel primo ciclo.

Proprio per informazione anche se - con un cronometro, piuttosto che il datetime e il numero di zecche misura:

Senza una chiamata al Console.Writeline prima del primo ciclo i tempi erano

 
for: 16802 
foreach: 2282 

con una chiamata a Console.Writeline erano

 
for: 2729 
foreach: 2268 

Anche se questi risultati non sono stati sempre ripetibile a causa del numero limitato di corse, ma t la grandezza della differenza era sempre più o meno la stessa.


Il codice modificato per riferimento:

 int[] x = new int[] { 3, 6, 9, 12 }; 
     int[] y = new int[] { 3, 6, 9, 12 }; 

     Console.WriteLine("Hello World"); 

     Stopwatch sw = new Stopwatch(); 

     sw.Start(); 
     for (int i = 0; i < 4; i++) 
     { 
      Console.WriteLine(x[i]); 
     } 
     sw.Stop(); 
     long elapsedTime = sw.ElapsedTicks; 

     sw.Reset(); 
     sw.Start(); 
     foreach (var item in y) 
     { 
      Console.WriteLine(item); 
     } 
     sw.Stop(); 
     long elapsedTime2 = sw.ElapsedTicks; 

     Console.WriteLine("\nSummary"); 
     Console.WriteLine("--------------------------\n"); 
     Console.WriteLine("for:\t{0}\nforeach:\t{1}", elapsedTime, elapsedTime2); 

     Console.ReadKey(); 
+1

Se potessi votare 100 volte lo farei. Questa è la risposta. Quindi, 2 problemi: 1. Usando la linea di scrittura in una sezione di codice si "profila" 2. JIT è il colpevole delle differenze. – Tim

+0

@ boat-programmer: Seconded! – Kredns

1

non vedo il motivo per cui tutti qui dice che for sarebbe più veloce di foreach in questo caso particolare.Per un List<T>, è (circa 2 volte più lento a foreach attraverso un elenco rispetto a for tramite un List<T>).

In effetti, lo sarà leggermente più veloce rispetto allo for qui. Poiché foreach su un array compila essenzialmente:

for(int i = 0; i < array.Length; i++) { } 

Uso .Length come criterio di arresto permette alla squadra di rimuovere e di confine controlli sull'accesso matrice, poiché è un caso speciale. Utilizzando i < 4, il JIT inserisce istruzioni aggiuntive per verificare ogni iterazione indipendentemente dal fatto che i non rientri nei limiti dell'array e genera un'eccezione in questo caso. Tuttavia, con .Length, è possibile garantire che non si andrà mai oltre i limiti dell'array in modo che i controlli dei limiti siano ridondanti, rendendolo più veloce.

Tuttavia, nella maggior parte dei loop, l'overhead del loop è insignificante rispetto al lavoro svolto all'interno.

La discrepanza che stai vedendo può essere spiegata solo dal JIT.

1

Non vorrei leggere troppo in questo - questo non è un buon codice di profilazione per i seguenti motivi
1. DateTime non è pensato per la profilazione. È necessario utilizzare QueryPerformanceCounter o StopWatch che utilizzano i contatori del profilo hardware della CPU
2. Console.WriteLine è un metodo dispositivo in modo che possano essere presenti effetti sottili come il buffering da prendere in considerazione
3. L'esecuzione di un'iterazione di ciascun blocco di codice non sarà mai ti dà risultati accurati perché la tua CPU fa un sacco di ottimizzazioni al volo come l'esecuzione fuori dagli ordini e la programmazione delle istruzioni
4. È probabile che il codice che ottiene JIT per entrambi i blocchi di codice sia abbastanza simile, quindi è probabile che sia nel cache istruzioni per il secondo blocco di codice

per avere una migliore idea di tempi, ho fatto la seguente

  1. sostituito il Console.WriteLine con un'espressione matematica (e^num)
  2. ho usato QueryPerformanceCounter/QueryPerformanceTimer attraverso P/Invoke
  3. ho corso ogni blocco di codice 1 milione di volte poi la media di risultati

Quando ho fatto che ho ottenuto i seguenti risultati:

il ciclo for ha 0.000676 millisecondi
Il ciclo foreach preso 0.000653 millisecondi

Così foreach era molto leggermente più veloce ma non di molto

Ho poi fatto alcuni ulteriori esperimenti e corse il blocco foreach primo e il per il secondo blocco
Quando ho fatto che ho ottenuto i seguenti risultati:

Il ciclo foreach ha impiegato 0,000702 millisecondi
Il ciclo for ha impiegato 0,000691 millisecondi

Infine ho eseguito entrambi i cicli insieme due volte.e per + foreach poi per + foreach nuovo
Quando ho fatto che ho ottenuto i seguenti risultati:

Il ciclo foreach preso 0.00140 millisecondi
Il ciclo ha preso 0,001,385 mila millisecondi

Quindi, fondamentalmente mi sembra che qualunque sia il codice che si esegue per secondo, viene eseguito leggermente più veloce ma non è sufficiente a per essere di alcun significato.
--Edit--
Qui ci sono un paio di link utili
How to time managed code using QueryPerformanceCounter
The instruction cache
Out of order execution

Problemi correlati