Pourquoi la seconde boucle for est-elle toujours exécutée plus rapidement que la première?

J’essayais de savoir si une boucle for était plus rapide qu’une boucle foreach et utilisais les classes System.Diagnostics pour chronométrer la tâche. En effectuant le test, j’ai remarqué que quelle que soit la boucle que je mette en premier, elle s’exécute toujours plus lentement que la dernière. Quelqu’un peut-il s’il vous plaît me dire pourquoi cela se produit? Mon code est ci-dessous:

using System; using System.Diagnostics; namespace cool { class Program { static void Main(ssortingng[] 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(); } } } 

Voici la sortie:

 for: 00:00:00.0175781 foreach: 00:00:00.0009766 

Probablement parce que les classes (par exemple, Console) doivent être compilées JIT pour la première fois. Vous obtiendrez les meilleures mesures en appelant d’abord toutes les méthodes (pour les JIT (échauffement puis réchauffement)), puis en effectuant le test.

Comme d’autres utilisateurs l’ont indiqué, 4 laissez-passer ne suffiront jamais pour vous montrer la différence.

Incidemment, la différence de performance entre for et foreach sera négligeable et les avantages de lisibilité liés à son utilisation l’emporteront presque toujours sur les avantages de performance marginaux.

  1. Je ne voudrais pas utiliser DateTime pour mesurer les performances – essayez la classe Stopwatch .
  2. Mesurer avec seulement 4 passes ne vous donnera jamais un bon résultat. Mieux vaut utiliser plus de 100 000 passages (vous pouvez utiliser une boucle externe). Ne faites pas Console.WriteLine dans votre boucle.
  3. Encore mieux: utilisez un profileur (comme Redgate ANTS ou peut-être NProf)

Je ne suis pas vraiment en C #, mais quand je me souviens bien, Microsoft construisait des compilateurs “Just in Time” pour Java. Lorsqu’ils utilisent les mêmes techniques ou des techniques similaires en C #, il serait plutôt naturel que “certaines constructions arrivant en deuxième position se comportent plus rapidement”.

Par exemple, il se peut que le système JIT voit qu’une boucle est exécutée et décide d’adhoc à comstackr l’ensemble de la méthode. Par conséquent, lorsque la deuxième boucle est atteinte, elle est encore compilée et fonctionne beaucoup plus rapidement que la première. Mais ceci est une hypothèse plutôt simpliste de la mienne. Bien entendu, vous avez besoin d’une connaissance beaucoup plus approfondie du système d’exécution C # pour comprendre ce qui se passe. Il est également possible que la RAM-Page soit accédée en premier dans la première boucle et dans la seconde, elle se trouve toujours dans le cache du processeur.

Addon: L’autre commentaire qui a été fait: que le module de sortie puisse être JIT une première fois dans la première boucle me semble plus probable que ma première hypothèse. Les langues modernes sont simplement très complexes pour savoir ce qui se fait sous le capot. Aussi cette déclaration de ma part s’inscrit dans cette hypothèse:

Mais vous avez aussi des sorties de terminal dans vos boucles. Ils rendent les choses encore plus difficiles. Il se pourrait aussi que cela coûte un peu de temps d’ouvrir le terminal une première fois dans un programme.

Je faisais juste des tests pour obtenir de vrais chiffres, mais en attendant, Gaz m’a battu jusqu’à la réponse – l’appel à Console.Writeline est mis en attente dès le premier appel, vous payez donc ce coût dans la première boucle.

Juste pour information cependant – en utilisant un chronomètre plutôt que la date et l’heure et en mesurant le nombre de ticks:

Sans appel à Console.Writeline avant la première boucle, les temps ont été

 pour: 16802
 pour chaque: 2282

avec un appel à Console.Writeline ils étaient

 pour: 2729
 pour chaque: 2268

Bien que ces résultats ne soient pas toujours reproductibles en raison du nombre limité de passages, l’ampleur de la différence a toujours été à peu près la même.


Le code édité pour référence:

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

La raison en est qu’il y a plusieurs formes de surcharge dans la version foreach qui ne sont pas présentes dans la boucle for

  • Utilisation d’un identifiant.
  • Un appel de méthode supplémentaire pour chaque élément. Chaque élément doit être accessible sous le capot en utilisant IEnumerator.Current qui est un appel à une méthode. Parce que c’est sur une interface, il ne peut pas être en ligne. Cela signifie N appels de méthode où N est le nombre d’éléments de l’énumération. La boucle for utilise simplement et indexeur
  • Dans une boucle foreach, tous les appels passent par une interface. En général, cela un peu plus lent que par un type concret

Veuillez noter que les choses que j’ai énumérées ci-dessus ne représentent pas nécessairement des coûts énormes. Ce sont généralement des coûts très faibles qui peuvent consortingbuer à une faible différence de performances.

Notez également, comme Mehrdad l’a fait remarquer, que les compilateurs et JIT peuvent choisir d’optimiser une boucle foreach pour certaines structures de données connues telles qu’un tableau. Le résultat final peut simplement être une boucle for.

Remarque: votre repère de performance en général nécessite un peu plus de travail pour être précis.

  • Vous devez utiliser un chronomètre au lieu de DateTime. C’est beaucoup plus précis pour les repères de performance.
  • Vous devez effectuer le test plusieurs fois, pas une seule fois
  • Vous devez exécuter une exécution fictive sur chaque boucle pour éliminer les problèmes liés à la méthode JIT dès la première fois. Ce n’est probablement pas un problème quand tout le code est dans la même méthode mais ça ne fait pas mal.
  • Vous devez utiliser plus de 4 valeurs dans la liste. Essayez 40 000 à la place.

Vous devriez utiliser le chronomètre pour chronométrer le comportement.

Techniquement, la boucle for est plus rapide. Foreach appelle la méthode MoveNext () (créant une stack de méthodes et un surdébit à partir d’un appel) sur l’iterator de IEnumerable, lorsque seule l’incrémentation doit être incrémentée.

Je ne vois pas pourquoi tout le monde ici disait que cela serait plus rapide que dans le cas présent. Pour une List , il est (environ 2x plus lent à parcourir une liste que à une List ).

En fait, le foreach sera légèrement plus rapide que le for here. Parce que foreach sur un tableau se comstack essentiellement pour:

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

L'utilisation de .Length tant que critère d'arrêt permet au JIT de supprimer les vérifications de limites sur l'access au tableau, car il s'agit d'un cas particulier. L'utilisation de i < 4 oblige le JIT à insérer des instructions supplémentaires pour vérifier chaque itération si i est ou non hors des limites du tableau et, le cas échéant, émettre une exception. Cependant, avec .Length , il peut garantir que vous ne .Length jamais des limites du tableau, de sorte que les vérifications des limites sont redondantes, ce qui .Length .

Cependant, dans la plupart des boucles, la surcharge de la boucle est insignifiante comparée au travail effectué à l'intérieur.

La différence que vous constatez ne peut être expliquée que par l'EJI, je suppose.

Je ne lirais pas trop dans ceci – ce n’est pas un bon code de profilage pour les raisons suivantes
1. DateTime n’est pas destiné au profilage. Vous devez utiliser QueryPerformanceCounter ou StopWatch, qui utilise les compteurs de profils matériels de la CPU.
2. Console.WriteLine est une méthode de périphérique donc il peut y avoir des effets subtils tels que la mise en mémoire tampon à prendre en compte
3. L’exécution d’une itération de chaque bloc de code ne vous donnera jamais de résultats précis, car votre CPU effectue beaucoup d’optimisations funky à la volée, telles que l’exécution dans le désordre et la planification des instructions.
4. Il est fort probable que le code obtenu avec JIT pour les deux blocs de code soit relativement similaire, de sorte qu’il est susceptible de figurer dans le cache d’instructions pour le deuxième bloc de code

Pour avoir une meilleure idée du timing, j’ai fait ce qui suit

  1. Remplacement de Console.WriteLine par une expression mathématique (e ^ num)
  2. J’ai utilisé QueryPerformanceCounter / QueryPerformanceTimer via P / Invoke
  3. J’ai exécuté chaque bloc de code 1 million de fois, puis la moyenne des résultats

Quand j’ai fait ça, j’ai eu les résultats suivants:

La boucle for a pris 0,000676 milliseconde
La boucle foreach a pris 0,000653 milliseconde

Donc, foreach était très légèrement plus rapide mais pas beaucoup

J’ai ensuite fait quelques expériences supplémentaires et ai exécuté le bloc foreach en premier et le bloc en second
Quand j’ai fait ça, j’ai eu les résultats suivants:

La boucle foreach a pris 0,000702 millisecondes
La boucle for a pris 0,000691 milliseconds

Enfin, j’ai couru les deux boucles ensemble deux fois, c’est-à-dire pour + foreach puis pour + foreach à nouveau
Quand j’ai fait ça, j’ai eu les résultats suivants:

La boucle foreach a pris 0,00140 milliseconde
La boucle for a pris 0.001385 milliseconds

Donc, fondamentalement, il me semble que quel que soit le code que vous exécutez en deuxième position, il est légèrement plus rapide, mais insuffisant.
–Modifier–
Voici quelques liens utiles
Comment gérer le temps du code à l’aide de QueryPerformanceCounter
Le cache d’instruction
Exécution en panne