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();
}
Cela dépend en partie du type exact de list
. Cela dépendra également du CLR exact que vous utilisez.
Que ce soit de quelque manière que ce soit significatif ou non dépendra du fait que vous fassiez un travail réel dans la boucle. Dans presque tous les cas, la différence de performance ne sera pas significative, mais la différence de lisibilité privilégiera la boucle foreach
.
Personnellement, j'utiliserais LINQ pour éviter le "si" aussi:
foreach (var item in list.Where(condition))
{
}
EDIT: Pour ceux d'entre vous qui affirment que l'itération sur un List<T>
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<object> list)
{
foreach (object o in list)
{
Console.WriteLine(o);
}
}
Produit IL de:
.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
Le compilateur traite matrices différemment, convertissant une boucle foreach
en une boucle for
, mais pas List<T>
. Voici le code équivalent pour un tableau:
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
Fait intéressant, je ne peux pas trouver cela documenté dans la spécification C # 3 n'importe où ...
Une boucle for
est compilée dans un code à peu près équivalent à ceci:
int tempCount = 0;
while (tempCount < list.Count)
{
if (list[tempCount].value == value)
{
// Do something
}
tempCount++;
}
Où une boucle foreach
est compilée dans un code à peu près équivalent à ceci:
using (IEnumerator<T> e = list.GetEnumerator())
{
while (e.MoveNext())
{
T o = (MyClass)e.Current;
if (row.value == value)
{
// Do something
}
}
}
Comme vous pouvez le constater, tout dépend de la façon dont l’énumérateur est implémenté et de l’indexeur des listes. Comme il s'avère que l'énumérateur des types basés sur des tableaux est normalement écrit quelque chose comme ceci:
private static IEnumerable<T> MyEnum(List<T> list)
{
for (int i = 0; i < list.Count; i++)
{
yield return list[i];
}
}
Comme vous pouvez le constater, dans ce cas, cela ne fera pas beaucoup de différence, mais l'énumérateur d'une liste chaînée ressemblerait probablement à ceci:
private static IEnumerable<T> MyEnum(LinkedList<T> list)
{
LinkedListNode<T> current = list.First;
do
{
yield return current.Value;
current = current.Next;
}
while (current != null);
}
Dans .NET vous constaterez que la classe LinkedList <T> n’a même pas d’indexeur, vous ne pourrez donc pas faire votre boucle for sur une liste chaînée; mais si vous le pouviez, l'indexeur devrait être écrit comme suit:
public T this[int index]
{
LinkedListNode<T> current = this.First;
for (int i = 1; i <= index; i++)
{
current = current.Next;
}
return current.value;
}
Comme vous pouvez le constater, l'appel de plusieurs fois dans une boucle sera beaucoup plus lent que l'utilisation d'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(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();
}
Et voici la section foreach:
foreach (int i in intList)
{
int foo = i * 2;
if (foo % 2 == 0)
{
}
}
Lorsque j'ai remplacé le for par un foreach - celui-ci prenait 20 millisecondes plus vite - régulièrement . Le pour était 135-139ms tandis que le foreach était 113-119ms. J'ai échangé plusieurs fois, en m'assurant que ce n'était pas un processus qui venait de commencer.
Cependant, lorsque j'ai supprimé le foo et l'instruction if, le temps était plus rapide de 30 ms (pour 88 ms à 59 ms). Ils étaient tous deux des coquilles vides. Je suppose que le foreach a en fait passé une variable alors que le for ne faisait qu’incrémenter une variable. Si j'ai ajouté
int foo = intList[i];
Puis le pour devenir lent d'environ 30ms. Je suppose que cela avait à voir avec le fait de créer foo, de saisir la variable dans le tableau et de l'attribuer à 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 grand entier possible avant l’exception 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();
}
Et voici les résultats:
Génération de données . Calcul en boucle: 2458ms Calcul en boucle: 2005ms
Les échanger pour voir si cela concerne l'ordre des choses donne les mêmes résultats (presque).
Remarque: cette réponse s'applique plus à Java qu'à C #, car C # n'a pas d'indexeur sur LinkedLists
, mais je pense que le point général est toujours valable.
Si la list
avec laquelle vous travaillez est une LinkedList
, les performances du code-indexeur ( style-tableau accès) sont bien pires que si vous utilisiez la IEnumerator
à partir de la foreach
pour les grandes listes.
Lorsque vous accédez à l'élément 10.000 dans LinkedList
à l'aide de la syntaxe d'indexeur: list[10000]
, la liste liée commence au nœud principal et traverse le pointeur Next
- dix mille fois jusqu'à atteindre le bon objet. É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 syntaxe forach
-), vous obtenez un objet IEnumerator
qui a un pointeur sur le noeud principal. Chaque fois que vous appelez MoveNext
, ce pointeur est déplacé vers le nœud suivant, comme suit:
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 constater, dans le cas de LinkedList
s, la méthode d’indexation de tableau devient de plus en plus lente, plus la boucle est longue (elle doit parcourir le même pointeur en tête encore et encore). Alors que la IEnumerable
fonctionne simplement en temps constant.
Bien sûr, comme Jon a dit, cela dépend vraiment du type de list
, si le list
n'est pas un LinkedList
, mais un tableau, le comportement est complètement différent.
Comme d'autres personnes l'ont mentionné, même si les performances importent peu, 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é sont appelées dans la construction foreach.
IEnumerator iterator = ((IEnumerable)list).GetEnumerator();
while (iterator.MoveNext()) {
var item = iterator.Current;
// do stuff
}
C'est le développement équivalent 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. Alors que dans un accès à un tableau, vous n'avez pas de dépendances.
Après avoir lu suffisamment d'arguments pour que "la boucle foreach soit préférée pour la lisibilité", je peux dire que ma première réaction a été "quoi"? La lisibilité, en général, est subjective et, dans ce cas particulier, encore plus. Pour ceux qui ont une formation en programmation (pratiquement, tous les langages avant Java), les boucles for sont beaucoup plus faciles à lire que les boucles foreach. En outre, 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 susmentionné.
Pour en savoir plus sur l'impact sur les performances, voir la réponse à this question.
EDIT: Il y a 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 où, à mon avis, il devrait être utilisé sur pour.
Dans l'exemple que vous avez fourni, il est définitivement préférable d'utiliser la boucle foreach
à la place d'une boucle for
.
La construction standard foreach
peut être plus rapide (1,5 cycle par étape) qu'un simple for-loop
(2 cycles par étape), à moins que la boucle n'ait été déroulée (1,0 cycle par étape).
Ainsi, pour le code de tous les jours, les performances ne sont pas une raison pour utiliser les constructions plus complexes for
, while
ou do-while
.
Consultez ce lien: 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 ║
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝
vous pouvez lire à ce sujet dans Deep .NET - part 1 Itération
il couvre les résultats (sans la première initialisation) du code source .NET jusqu'au désassemblage.
par exemple - Itération de tableau avec une boucle foreach:
Il existe un autre fait intéressant qui peut être facilement omis lors du test de la vitesse des deux boucles: L'utilisation du mode débogage ne permet pas au compilateur d'optimiser le code en utilisant les paramètres par défaut.
Cela m'a conduit au résultat intéressant que le foreach est plus rapide que pour le mode débogage. Alors que le for est plus rapide que foreach en mode release. Il est évident que le compilateur dispose de meilleurs moyens d'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 que celle-ci soit même optimisée par le processeur lui-même.