Quel extrait de code donnera de meilleures performances? Les segments de code ci-dessous ont été écrits en C #.
1.
for(int counter=0; counter<list.Count; counter++) { list[counter].DoSomething(); }
2.
foreach(MyType current in list) { current.DoSomething(); }
Eh bien, cela dépend en partie du type exact de list
. Cela dépendra également du CLR exact que vous utilisez.
Que ce soit d’une manière ou d’une autre significatif, cela dépendra si vous faites un vrai travail dans la boucle. Dans presque tous les cas, la différence de performance ne sera pas significative, mais la différence de lisibilité favorise la boucle foreach
.
J’utiliserais personnellement LINQ pour éviter le “if” aussi:
foreach (var item in list.Where(condition)) { }
EDIT: Pour ceux d’entre vous qui prétendent que l’itération sur une List
avec foreach
produit le même code que la boucle for
, voici la preuve que ce n’est pas le cas:
static void IterateOverList(List
Produit IL de:
.method private hidebysig static void IterateOverList(class [mscorlib]System.Collections.Generic.List`1
Le compilateur traite les tableaux différemment, convertissant une boucle foreach
en une boucle for
, mais pas la List
. Voici le code équivalent pour un tableau:
static void IterateOverArray(object[] array) { foreach (object o in array) { Console.WriteLine(o); } } // Comstacks 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
Fait intéressant, je ne trouve pas cela documenté dans la spécification C # 3 où que ce soit …
Une boucle for
est compilée pour coder approximativement l’équivalent de ceci:
int tempCount = 0; while (tempCount < list.Count) { if (list[tempCount].value == value) { // Do something } tempCount++; }
Où une boucle foreach
est compilée pour coder approximativement l'équivalent de ceci:
using (IEnumerator e = list.GetEnumerator()) { while (e.MoveNext()) { T o = (MyClass)e.Current; if (row.value == value) { // Do something } } }
Comme vous pouvez le voir, tout dépend de la manière dont l’énumérateur est implémenté et de la manière dont l’indexeur de listes est implémenté. Comme il s'avère que l'énumérateur pour les types basés sur des tableaux est normalement écrit comme ceci:
private static IEnumerable MyEnum(List list) { for (int i = 0; i < list.Count; i++) { yield return list[i]; } }
Donc, comme vous pouvez le voir, dans cet exemple, cela ne fera pas beaucoup de différence, cependant l'énumérateur d'une liste liée ressemblera probablement à ceci:
private static IEnumerable MyEnum(LinkedList list) { LinkedListNode current = list.First; do { yield return current.Value; current = current.Next; } while (current != null); }
Dans .NET, vous constaterez que la classe LinkedList
public T this[int index] { LinkedListNode current = this.First; for (int i = 1; i <= index; i++) { current = current.Next; } return current.value; }
Comme vous pouvez le voir, le fait d'appeler cela plusieurs fois dans une boucle sera beaucoup plus lent que d'utiliser un énumérateur qui peut se rappeler où il se trouve dans la liste.
Un test facile à semi-valider. J’ai fait un petit test, juste pour voir. Voici le code:
static void Main(ssortingng[] args) { List intList = new List (); 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(); }
Et voici la section foreach:
foreach (int i in intList) { int foo = i * 2; if (foo % 2 == 0) { } }
Lorsque j'ai remplacé le pour avec un foreach - le foreach était 20 millisecondes plus vite - de manière cohérente . Le pour 135-139ms alors que le foreach était 113-119ms. J'ai échangé plusieurs fois, en m'assurant que ce n'était pas un processus qui venait d'être lancé.
Cependant, lorsque j'ai supprimé le foo et l'instruction if, le for était plus rapide de 30 ms (foreach était de 88 ms et était de 59 ms). Ils étaient tous les deux des coquilles vides. Je suppose que le foreach a effectivement passé une variable où le for incrémentait simplement une variable. Si j'ai ajouté
int foo = intList[i];
Ensuite, le pour devenir lent d'environ 30ms. Je suppose que cela a à voir avec la création de foo et en saisissant la variable dans le tableau et en l'affectant à foo. Si vous accédez simplement à intList [i], vous n'avez pas cette pénalité.
En toute honnêteté .. Je m'attendais à ce que le foreach soit légèrement plus lent en toutes circonstances, mais pas assez pour avoir de l'importance dans la plupart des applications.
edit: voici le nouveau code utilisant les suggestions de Jons (134217728 est le plus gros int que vous puissiez avoir avant que l'exception System.OutOfMemory ne soit lancée):
static void Main(ssortingng[] args) { List intList = new List (); 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(); }
Et voici les résultats:
Générer des données. Calcul pour boucle: 2458ms Calcul de la boucle foreach: 2005ms
Les échanger pour voir s'il s'agit de l'ordre des choses donne les mêmes résultats (presque).
Note: cette réponse s’applique plus à Java qu’à C #, puisque C # n’a pas d’indexeur sur LinkedLists
, mais je pense que le point général est toujours valable.
Si la list
vous travaillez est une LinkedList
, la performance du code indexeur (access de type tableau ) est bien pire que d’utiliser IEnumerator
partir de foreach
, pour les grandes listes.
Lorsque vous accédez à l’élément 10.000 dans une LinkedList
à l’aide de la syntaxe de l’indexeur: list[10000]
, la liste liée démarre au nœud principal et traverse le pointeur Next
dix mille fois, jusqu’à ce qu’il atteigne l’object correct. Évidemment, si vous faites cela en boucle, vous obtiendrez:
list[0]; // head list[1]; // head.Next list[2]; // head.Next.Next // etc.
Lorsque vous appelez GetEnumerator
(en utilisant implicitement la forach
forach), vous obtenez un object IEnumerator
doté d’un pointeur sur le nœud principal. Chaque fois que vous appelez MoveNext
, ce pointeur est déplacé vers le nœud suivant, comme MoveNext
:
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.
Comme vous pouvez le voir, dans le cas de LinkedList
s, la méthode de l’indexeur de tableau devient de plus en plus lente, plus vous bouclez longtemps (il doit passer le même pointeur maintes et maintes fois). Alors que le IEnumerable
fonctionne simplement en temps constant.
Bien sûr, comme Jon a dit que cela dépend vraiment du type de list
, si la list
n’est pas une LinkedList
, mais un tableau, le comportement est complètement différent.
Comme d’autres personnes l’ont mentionné, même si les performances n’ont pas vraiment d’importance, le foreach sera toujours un peu plus lent en raison de l’utilisation de IEnumerable
/ IEnumerator
dans la boucle. Le compilateur traduit la construction en appels sur cette interface et pour chaque étape, une fonction + une propriété est appelée dans la construction foreach.
IEnumerator iterator = ((IEnumerable)list).GetEnumerator(); while (iterator.MoveNext()) { var item = iterator.Current; // do stuff }
C’est l’expansion équivalente de la construction en C #. Vous pouvez imaginer comment l’impact sur les performances peut varier en fonction des implémentations de MoveNext et Current. Considérant que dans un access au tableau, vous n’avez pas ces dépendances.
Après avoir lu suffisamment d’arguments pour dire que “la boucle foreach devrait être préférée pour la lisibilité”, je peux dire que ma première réaction a été “what”? La lisibilité, en général, est subjective et, dans ce cas particulier, encore plus. Pour une personne ayant une formation en programmation (pratiquement toutes les langues avant Java), les boucles sont beaucoup plus faciles à lire que les boucles foreach. De plus, les mêmes personnes affirmant que les boucles foreach sont plus lisibles sont également des partisans de linq et d’autres «fonctionnalités» qui rendent le code difficile à lire et à maintenir, ce qui prouve le point ci-dessus.
À propos de l’impact sur les performances, voir la réponse à cette question.
EDIT: Il existe des collections en C # (comme le HashSet) qui n’ont pas d’indexeur. Dans ces collections, foreach est le seul moyen d’itérer et c’est le seul cas pour lequel je pense qu’il devrait être utilisé.
Il y a un autre fait intéressant qui peut être facilement manqué lors du test de la vitesse des deux boucles: L’utilisation du mode de débogage ne permet pas au compilateur d’optimiser le code en utilisant les parameters par défaut.
Cela m’a conduit à un résultat intéressant: le foreach est plus rapide que dans le mode debug. Considérant que le for est plus rapide que foreach dans le mode de libération. De toute évidence, le compilateur dispose de meilleurs moyens pour optimiser une boucle for qu’une boucle foreach qui compromet plusieurs appels de méthode. Une boucle for est par ailleurs tellement fondamentale qu’il est possible qu’elle soit même optimisée par le processeur lui-même.
Dans l’exemple que vous avez fourni, il est préférable d’utiliser une boucle foreach
au lieu d’une boucle for
.
La construction foreach
standard peut être plus rapide (1,5 cycle par étape) qu’une simple for-loop
(2 cycles par étape), à moins que la boucle n’ait été déroulée (1,0 cycle par étape).
Donc, pour le code de tous les jours, la performance n’est pas une raison d’utiliser les structures plus complexes for
, while
ou rien.
Consultez ce lien: http://www.codeproject.com/Articles/146797/Fast-and-Less-Fast-Loops-in-C
╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗ ║ Method ║ List ║ int[] ║ Ilist onList ║ Ilist 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 ║ ╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝