web-dev-qa-db-fra.com

Différence de performance pour les structures de contrôle 'pour' et 'foreach' en C #

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();
}
102
Kthevar

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ù ...

129
Jon Skeet

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.

14
Martin Brown

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).

12
Kenny Mann

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

9
Tom Lokhorst

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.

2

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.

1
ThunderGr

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

0
rpax

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:  enter image description here

et - liste d'itérations avec boucle foreach:  enter image description here

et les résultats finaux:  enter image description here

 enter image description here

0
Or Yaacov

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.

0
Sam