Différence de performances pour les structures de contrôle «for» et «foreach» en C #

105

Quel extrait de code offrira 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();
}
Kthevar
la source
31
J'imagine que cela n'a pas vraiment d'importance. Si vous rencontrez des problèmes de performances, ce n'est certainement pas à cause de cela. Pas que vous ne devriez pas poser la question ...
darasd
2
À moins que votre application ne soit très critique en termes de performances, je ne m'inquiéterais pas à ce sujet. Il est préférable d'avoir un code propre et facilement compréhensible.
Fortyrunner le
2
Cela m'inquiète que certaines des réponses ici semblent être postées par des personnes qui n'ont tout simplement pas le concept d'itérateur nulle part dans leur cerveau, et donc aucun concept d'énumérateurs ou de pointeurs.
Ed James le
3
Ce 2ème code ne se compilera pas. System.Object n'a aucun membre appelé «valeur» (à moins que vous ne soyez vraiment méchant, que vous l'ayez défini comme une méthode d'extension et que vous compariez des délégués). Tapez fortement votre foreach.
Trillian le
1
Le premier code ne se compilera pas non plus, sauf si le type de a listvraiment un countmembre à la place de Count.
Jon Skeet

Réponses:

130

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 significatif ou non dépendra du fait que vous effectuez un vrai travail en boucle. Dans presque tous les cas, la différence de performance ne sera pas significative, mais la différence de lisibilité favorise la foreachboucle.

J'utiliserais personnellement LINQ pour éviter aussi le "si":

foreach (var item in list.Where(condition))
{
}

EDIT: Pour ceux d'entre vous qui prétendent que l'itération sur un List<T>avec foreachproduit le même code que la forboucle, 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 les tableaux différemment, convertissant une foreachboucle en une forboucle, 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 trouver cela documenté dans la spécification C # 3 nulle part ...

Jon Skeet
la source
Par intérêt Jon, le scénario avec List <T> ci-dessus ... cela s'applique-t-il également à d'autres collections? Aussi, comment saviez-vous cela (sans aucune intention malveillante) ... comme dans ... avez-vous littéralement trébuché sur cela en essayant de répondre à cette question, il y a quelque temps auparavant? C'est tellement ... aléatoire / secret :)
Pure.Krome
5
J'ai été au courant des optimisations de tableau pendant un certain temps - les tableaux sont une sorte de collection «principale»; le compilateur C # en est déjà profondément conscient, il est donc logique qu'il les traite différemment. Le compilateur n'a pas (et ne devrait pas) avoir de connaissances particulières sur List<T>.
Jon Skeet
Cheers :) et ouais ... les tableaux ont été le premier concept de collection que l'on m'a enseigné il y a des années et des années à l'université. collection. bravo encore!
Pure.Krome
3
@JonSkeet L'optimisation de l'itérateur de liste change le comportement lorsque la liste est modifiée pendant l'itération. Vous perdez exception-si-modifié. Il est toujours possible d'optimiser, mais nécessite de vérifier qu'aucune modification ne se produit (y compris sur d'autres threads, je suppose).
Craig Gidney
5
@VeeKeyBee: C'est ce que disait Microsoft en 2004. a) les choses changent; b) le travail devrait être un travail minime à chaque itération pour que cela soit significatif. Notez que foreachsur un tableau équivaut de fortoute façon. Commencez toujours par coder pour la lisibilité, puis ne micro-optimisez que lorsque vous avez la preuve que cela donne un avantage mesurable en termes de performances.
Jon Skeet
15

Une forboucle est compilée en code à peu près équivalent à ceci:

int tempCount = 0;
while (tempCount < list.Count)
{
    if (list[tempCount].value == value)
    {
        // Do something
    }
    tempCount++;
}

Où une foreachboucle est compilée en 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
        }
    }
}

Donc, comme vous pouvez le voir, tout dépendrait de la façon dont l'énumérateur est implémenté par rapport à la façon dont l'indexeur de listes est implémenté. Il s'avère que l'énumérateur pour les 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];
    }
}

Donc, comme vous pouvez le voir, dans ce cas, cela ne fera pas beaucoup de différence, mais l'énumérateur d'une liste lié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 liée; mais si vous pouviez, l'indexeur devrait être écrit comme ceci:

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 voir, 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.

Martin Brown
la source
12

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 - le foreach était 20 millisecondes plus rapide - de manière cohérente . Le pour était de 135-139ms tandis que le foreach était de 113-119ms. J'ai échangé plusieurs fois d'avant en arrière, m'assurant que ce n'était pas un processus qui venait juste de démarrer.

Cependant, lorsque j'ai supprimé l'instruction foo et if, le for était plus rapide de 30 ms (foreach était de 88 ms et for était de 59 ms). C'étaient tous les deux des coquilles vides. Je suppose que le foreach a en fait passé une variable alors que le for incrémentait simplement une variable. Si j'ai ajouté

int foo = intList[i];

Ensuite, le for devient lent d'environ 30 ms. Je suppose que cela a à voir avec la création de foo et la saisie de la variable dans le tableau et son affectation à 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 dans toutes les circonstances, mais pas assez pour avoir de l'importance dans la plupart des applications.

edit: voici le nouveau code utilisant les suggestions Jons (134217728 est le plus grand int que vous puissiez avoir avant que l'exception System.OutOfMemory ne soit levée):

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érer des données. Calcul de la boucle for: 2458ms Calcul de la boucle foreach: 2005ms

Les échanger pour voir si cela traite de l'ordre des choses donne (presque) les mêmes résultats.

Kenny Mann
la source
6
Il vaut mieux utiliser Stopwatch que DateTime.Now - et je ne ferais pas confiance à une course aussi rapide, pour être honnête.
Jon Skeet le
8
Vos boucles foreach s'exécutent plus rapidement car un «pour» évalue la condition à chaque itération. Dans le cas de votre exemple, cela fait un appel de méthode supplémentaire (pour obtenir list.count) En bref, vous comparez deux morceaux de code différents, d'où vos résultats étranges. Essayez 'int max = intlist.Count; for (int i = 0; i <max; i ++) ... 'et la boucle' for 's'exécutera toujours plus vite, comme prévu!
AR
1
Après compilation, for et foreach s'optimisent exactement de la même manière lorsque vous travaillez avec des primitives. Ce n'est que lorsque vous introduisez List <T> qu'ils diffèrent (grandement) en vitesse.
Anthony Russell
9

Remarque: cette réponse s'applique plus à Java qu'à C #, car C # n'a pas d'indexeur LinkedLists, mais je pense que le point général est toujours valable.

Si listvous travaillez avec un LinkedList, les performances du code indexeur ( accès de type tableau ) sont bien pires que l'utilisation de IEnumeratorfrom the foreach, pour de grandes listes.

Lorsque vous accédez à l'élément 10.000 dans a en LinkedListutilisant la syntaxe de l'indexeur:, list[10000]la liste chaînée commencera au nœud principal et traversera le Nextpointeur dix mille fois, jusqu'à ce qu'elle atteigne l'objet 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 forachsyntaxe -syntaxe), vous obtiendrez un IEnumeratorobjet qui a un pointeur vers le nœud principal. Chaque fois que vous appelez MoveNext, ce pointeur est déplacé vers le nœud suivant, comme ceci:

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 LinkedLists, la méthode d'indexation de tableau devient de plus en plus lente, plus la boucle est longue (elle doit passer par le même pointeur de tête encore et encore). Alors que le IEnumerablejuste fonctionne en temps constant.

Bien sûr, comme Jon l'a dit, cela dépend vraiment du type de list, si le listn'est pas un LinkedList, mais un tableau, le comportement est complètement différent.

Tom Lokhorst
la source
4
LinkedList dans .NET n'a pas d'indexeur, donc ce n'est pas réellement une option.
Jon Skeet le
Oh, eh bien, cela résout ce problème, alors :-) Je regarde juste la LinkedList<T>documentation sur MSDN, et il a une API assez décente. Plus important encore, il n'a pas de get(int index)méthode, comme Java. Pourtant, je suppose que le point est toujours valable pour toute autre structure de données de type liste qui expose un indexeur plus lent qu'un spécifique IEnumerator.
Tom Lokhorst le
2

Comme d'autres personnes l'ont mentionné, bien que les performances n'aient pas vraiment d'importance, le foreach sera toujours un peu plus lent à cause de l' utilisation IEnumerable/ IEnumeratordans 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 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 ces dépendances.

Charles Prakash Dasari
la source
4
N'oubliez pas qu'il y a une différence entre un accès tableau et un accès indexeur. Si list est List<T>ici, alors il y a toujours le hit (éventuellement en ligne) d'appeler l'indexeur. Ce n'est pas comme s'il s'agissait d'un accès à un tableau nu.
Jon Skeet le
Très vrai! C'est encore une autre exécution de propriété et nous sommes à la merci de la mise en œuvre.
Charles Prakash Dasari le
1

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é "quoi"? La lisibilité, en général, est subjective et, dans ce cas particulier, encore plus. Pour quelqu'un avec une formation en programmation (pratiquement, tous les langages avant Java), les boucles for 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, voyez 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 il est le seul cas , je pense qu'il devrait être utilisé plus pour .

ThunderGr
la source
0

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 paramètres par défaut.

Cela m'a conduit au résultat intéressant que foreach est plus rapide qu'en mode débogage. Alors que le for est plus rapide que foreach en mode release. De toute évidence, le compilateur a de meilleures façons d'optimiser une boucle for qu'une boucle foreach qui compromet plusieurs appels de méthode. Une boucle for est d'ailleurs si fondamentale qu'il est possible qu'elle soit même optimisée par le processeur lui-même.

Sam
la source
0

Dans l'exemple que vous avez fourni, il est certainement préférable d'utiliser une foreachboucle plutôt qu'une forboucle.

La foreachconstruction standard peut être plus rapide (1,5 cycles 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, les performances ne sont pas une raison d'utiliser les plus complexes for, whileou des do-whileconstructions.

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               
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝

rpax
la source
4
Vous pouvez relire l'article de projet de code que vous avez lié. C'est un article intéressant, mais il dit exactement le contraire de votre message. En outre, la table que vous avez recréée mesure les performances d'accès à un tableau et à une liste directement, ou via leurs interfaces IList. Ni l'un ni l'autre n'ont rien à voir avec la question. :)
Paul Walls
0

vous pouvez en savoir plus sur Deep .NET - partie 1 Itération

il couvre les résultats (sans la première initialisation) du code source .NET jusqu'au désassemblage.

par exemple - Array Itération avec une boucle foreach: entrez la description de l'image ici

et - itération de liste avec la boucle foreach: entrez la description de l'image ici

et les résultats finaux: entrez la description de l'image ici

entrez la description de l'image ici

Ou Yaacov
la source