Différence de Performance pour les structures de contrôle "for" et "foreach" en C#

quel morceau 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();
}
101
demandé sur 0lukasz0 2009-07-14 15:16:33

9 réponses

Eh bien, cela dépend en partie du type exact de list . Cela dépendra aussi du CLR exact que vous utilisez.

que ce soit d'une manière ou d'une autre significatif ou non dépendra de si vous faites 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é favorise la boucle foreach .

utilisez LINQ personnellement pour éviter le "SI" aussi:

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

éditer: pour ceux d'entre vous qui prétendent que itérer sur un List<T> avec foreach produit le même code que la boucle for , voici la preuve qu'il ne le fait pas:

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 tableaux différemment, en convertissant une boucle foreach essentiellement en une for boucle, 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 trouve pas cela documenté dans la spécification C# 3 nulle part...

127
répondu Jon Skeet 2009-07-14 11:58:05

a for loop est compilé pour coder approximativement équivalent à ceci:

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

où comme une boucle foreach est compilé pour coder approximativement é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 voir, tout dépend de la façon dont l'agent recenseur est mis en œuvre par rapport à la façon dont l'indexeur listes est mis en œuvre. Comme il s'avère que l'énumérateur pour les types basés sur des tableaux sont 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, il ne fera pas beaucoup de différence, mais l'énumérateur pour une liste liée ressemblerait probablement quelque chose comme 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 trouverez que la classe LinkedList n'a même pas d'indexeur, de sorte que vous ne seriez pas en mesure de faire votre boucle for sur une liste liée; mais si vous le pouviez, l'indexeur devrait être écrit comme ceci: "15198090920"

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, l'appeler plusieurs fois dans une boucle va être beaucoup plus lent que d'utiliser un énumérateur qui peut se rappeler où il est dans la liste.

14
répondu Martin Brown 2012-06-13 17:26:53

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)
    {
    }
}

quand j'ai remplacé le for par un foreach -- l'foreach était 20 millisecondes plus rapide -- toujours . Le for était de 135-139ms tandis que le foreach était de 113-119ms. J'ai échangé plusieurs fois, pour m'assurer que ce n'était pas un processus qui venait d'entrer en jeu.

cependant, lorsque j'ai retiré la foo et la déclaration if, LE for était plus rapide de 30 ms (foreach était de 88ms Et for était de 59ms). Ils étaient tous les deux des coquilles vides. Je suppose que le foreach a en fait passé une variable où le for incrémentait juste une variable. Si j'ai ajouté

int foo = intList[i];

puis le for devient lent d'environ 30ms. Je suppose que ça a à voir avec la création de foo et le fait de saisir la variable dans le tableau et de l'assigner à foo. Si vous venez de accéder à intList [i] alors 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 compter dans la plupart des applications.

edit: voici le nouveau code utilisant les suggestions de Jons (134217728 est le plus grand int que vous pouvez avoir avant le système.

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

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

12
répondu Kenny Mann 2012-06-08 07:40:27

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 tient toujours.

si le list avec lequel vous travaillez se trouve être un LinkedList , la performance de l'indexeur-code ( array-style accéder) est bien pire que d'utiliser le IEnumerator du foreach , pour les grandes listes.

Lorsque vous accédez à l'élément 10.000 dans un LinkedList en utilisant la syntaxe de l'indexeur: list[10000] , la liste liée commencera par le noeud principal, et traversera le pointeur Next dix mille fois, jusqu'à ce qu'il atteigne 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 (implicitement en utilisant la syntaxe forach ), vous obtiendrez un objet IEnumerator qui a un pointeur vers le noeud principal. Chaque fois que vous appelez MoveNext , ce pointeur est déplacé vers le prochain noeud, 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 LinkedList s, la méthode de l'indexeur de tableau devient de plus en plus lente, plus vous bouclez la boucle (il doit passer par le même pointeur de tête encore et encore). Alors que le IEnumerable ne fonctionne qu'en temps constant.

bien sûr, comme Jon l'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
répondu Tom Lokhorst 2009-07-14 14:03:49

comme d'autres personnes l'ont mentionné, bien que la performance n'ait pas vraiment d'importance, le foreach sera toujours un peu plus lent à cause de l'utilisation 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 d'avant.

IEnumerator iterator = ((IEnumerable)list).GetEnumerator();
while (iterator.MoveNext()) {
  var item = iterator.Current;
  // do stuff
}

il s'agit de l'extension équivalente de la construction en C#. Vous pouvez imaginer l'impact de la performance peut varier en fonction des implémentations de MoveNext et de Current. Alors que dans un accès de tableau, vous n'avez pas ces dépendances.

2
répondu Charles Prakash Dasari 2011-11-21 17:17:38

après avoir lu assez d'arguments que "la boucle foreach devrait être préférée pour la lisibilité", je peux dire que ma première réaction était "quoi"? La lisibilité, en général, est subjective et, dans ce cas particulier, même plus. Pour quelqu'un avec un arrière-plan dans la programmation(pratiquement, chaque langue avant Java), les boucles Foreach sont beaucoup plus faciles à lire que les boucles foreach. En outre, les mêmes personnes affirmant que chaque boucle sont plus lisibles, sont également des partisans de linq et d'autres les "traits" qui rendent le code difficile à lire et à maintenir, quelque chose qui prouve le point ci-dessus.

à propos de l'impact sur la performance, voir la réponse à cette question .

EDIT: il y a des collections en C#(comme le HashSet) qui n'ont pas d'indexeur. Dans ces collections, foreach est la seule façon d'itérer et c'est le seul cas, je pense qu'il devrait être utilisé plus de pour .

1
répondu ThunderGr 2017-05-23 11:46:26

Il ya un autre fait intéressant qui peut être facilement raté lors de l'essai 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 le foreach est plus rapide qu'en mode de débogage. Alors que le for Est plus rapide que l'foreach dans le mode de libération. Évidemment, le compilateur a de meilleures façons d'optimiser une boucle for que d'une boucle foreach qui compromet plusieurs les appels de méthode. Une boucle for Est d'ailleurs tellement fondamentale qu'il est possible qu'elle soit même optimisée par le CPU lui-même.

0
répondu Sam 2013-08-29 13:28:08

dans l'exemple que vous avez fourni, il est certainement préférable d'utiliser une boucle foreach à la place d'une boucle for .

la construction standard foreach peut être plus rapide (1,5 cycle par pas) qu'une simple for-loop (2 cycles par pas), à moins que la boucle n'ait été déroulée (1,0 cycle par pas).

ainsi, pour le code quotidien, la performance n'est pas une raison d'utiliser les constructions plus complexes for , while ou do-while .

Regardez 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
répondu rpax 2014-02-23 17:38:03

vous pouvez le lire dans Deep .NET - Partie 1 itération

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

par exemple-itération en réseau avec boucle foreach: enter image description here

et liste itération avec boucle foreach: enter image description here

et les résultats finaux: enter image description here

enter image description here

0
répondu Or Yaacov 2018-09-24 11:59:47