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.
Stopwatch
. Console.WriteLine
dans votre boucle. 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
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 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 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
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