Prestatieverschil voor besturingsstructuren ‘for’ en ‘foreach’ in C#

Welk codefragment levert betere prestaties? De onderstaande codesegmenten zijn geschreven in C#.

1.

for(int tempCount=0;tempCount<list.count;tempcount++)
{
    if(list[tempCount].value==value)
    {
        // Some code.
    }
}
foreach(object row in list)
{
    if(row.value==value)
    {
        //Some coding
    }
}

Antwoord 1, autoriteit 100%

Nou, het hangt gedeeltelijk af van het exacte type list. Het hangt ook af van de exacte CLR die u gebruikt.

Of het op enigerlei wijze aanzienlijkis of niet, hangt ervan af of je in de loop van de tijd echt werk doet. In bijna allegevallen zal het verschil in prestatie niet significant zijn, maar het verschil in leesbaarheid is gunstig voor de foreach-lus.

Persoonlijk zou ik LINQ gebruiken om de “als” ook te vermijden:

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

EDIT: voor degenen onder u die beweren dat het herhalen van een List<T>met foreachdezelfde code oplevert als de forloop, hier is bewijs dat dit niet het geval is:

static void IterateOverList(List<object> list)
{
    foreach (object o in list)
    {
        Console.WriteLine(o);
    }
}

Produceert IL van:

.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

De compiler behandelt arraysanders, en converteert een foreach-lus in principe naar een for-lus, maar niet List<T>. Hier is de equivalente code voor een array:

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

Interessant genoeg kan ik dit nergens in de C# 3-specificatie vinden…


Antwoord 2, autoriteit 11%

Een for-lus wordt gecompileerd tot code die ongeveer gelijk is aan deze:

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

Waar als een foreach-lus wordt gecompileerd tot code die ongeveer gelijk is aan deze:

using (IEnumerator<T> e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        T o = (MyClass)e.Current;
        if (row.value == value)
        {
            // Do something
        }
    }
}

Zoals je kunt zien, hangt het er allemaal van af hoe de enumerator is geïmplementeerd en hoe de lijsten-indexer is geïmplementeerd. Het blijkt dat de enumerator voor typen op basis van arrays normaal gesproken ongeveer als volgt wordt geschreven:

private static IEnumerable<T> MyEnum(List<T> list)
{
    for (int i = 0; i < list.Count; i++)
    {
        yield return list[i];
    }
}

Zoals je kunt zien, maakt het in dit geval niet veel uit, maar de enumerator voor een gekoppelde lijst ziet er waarschijnlijk ongeveer zo uit:

private static IEnumerable<T> MyEnum(LinkedList<T> list)
{
    LinkedListNode<T> current = list.First;
    do
    {
        yield return current.Value;
        current = current.Next;
    }
    while (current != null);
}

In .NETvindt u dat de LinkedList<T> class heeft niet eens een indexer, dus je zou je for-lus niet kunnen doen op een gelinkte lijst; maar als je dat zou kunnen, zou de indexer zo moeten worden geschreven:

public T this[int index]
{
       LinkedListNode<T> current = this.First;
       for (int i = 1; i <= index; i++)
       {
            current = current.Next;
       }
       return current.value;
}

Zoals je kunt zien, zal het meerdere keren in een lus aanroepen veel langzamer zijn dan het gebruik van een enumerator die kan onthouden waar het zich in de lijst bevindt.


Antwoord 3, autoriteit 9%

Een eenvoudige test om semi-valideren. Ik heb een kleine test gedaan, gewoon om te zien. Hier is de 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();
}

En hier is de voorpagina:

foreach (int i in intList)
{
    int foo = i * 2;
    if (foo % 2 == 0)
    {
    }
}

Toen ik de for verving door een foreach — de foreach was 20 milliseconden sneller — consistent. De for was 135-139 ms, terwijl de foreach 113-119 ms was. Ik heb verschillende keren heen en weer gewisseld, om er zeker van te zijn dat het niet een of ander proces was dat net begon.

Toen ik echter de foo en de if-statement verwijderde, was de for 30 ms sneller (foreach was 88 ms en for was 59 ms). Het waren allebei lege hulzen. Ik ga ervan uit dat de foreach daadwerkelijk een variabele heeft doorgegeven, terwijl de for gewoon een variabele verhoogde. Als ik heb toegevoegd

int foo = intList[i];

Vervolgens wordt het ongeveer 30 ms traag. Ik neem aan dat dit te maken had met het maken van foo en het pakken van de variabele in de array en het toewijzen aan foo. Als je alleen intList[i] opent, heb je die straf niet.

Eerlijk gezegd.. Ik had verwacht dat de foreach onder alle omstandigheden iets langzamer zou zijn, maar niet genoeg om in de meeste toepassingen van belang te zijn.

edit: hier is de nieuwe code die Jons-suggesties gebruikt (134217728 is de grootste int die je kunt hebben voordat de uitzondering System.OutOfMemory wordt gegenereerd):

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

En hier zijn de resultaten:

Gegevens genereren.
Berekenen voor lus: 2458ms
Foreach-lus berekenen: 2005ms

Als je ze omwisselt om te zien of het omgaat met de volgorde van de dingen, krijg je (bijna) dezelfde resultaten.


Antwoord 4, autoriteit 7%

Opmerking: dit antwoord is meer van toepassing op Java dan op C#, aangezien C# geen indexer heeft op LinkedLists, maar ik denk dat het algemene punt nog steeds geldt.

Als de listwaarmee u werkt een LinkedListis, kan de prestatie van de indexer-code (array-stijldie toegang ) is veel erger dan het gebruik van de IEnumeratorvan de foreach, voor grote lijsten.

Wanneer u element 10.000 in een LinkedListopent met behulp van de indexersyntaxis: list[10000], begint de gekoppelde lijst bij het hoofdknooppunt en doorloopt de Next-pointer tienduizend keer, totdat het het juiste object bereikt. Als je dit in een lus doet, krijg je uiteraard:

list[0]; // head
list[1]; // head.Next
list[2]; // head.Next.Next
// etc.

Wanneer u GetEnumeratoraanroept (impliciet met behulp van de forach-syntaxis), krijgt u een IEnumerator-object met een verwijzing naar de hoofd knooppunt. Elke keer dat u MoveNextaanroept, wordt die aanwijzer verplaatst naar het volgende knooppunt, zoals:

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.

Zoals je kunt zien, wordt in het geval van LinkedLists de array-indexermethode langzamer en langzamer, hoe langer je lus (hij moet steeds opnieuw door dezelfde hoofdaanwijzer gaan) . Terwijl de IEnumerablegewoon in constante tijd werkt.

Natuurlijk, zoals Jon zei, hangt dit echt af van het type list, als de listgeen LinkedListis, maar een array , het gedrag is compleet anders.


Antwoord 5, autoriteit 2%

Zoals andere mensen al hebben gezegd, hoewel de prestaties er niet echt toe doen, zal de foreach altijd een beetje langzamer zijn vanwege het gebruik van IEnumerable/IEnumeratorin de lus. De compiler vertaalt de constructie naar aanroepen op die interface en voor elke stap wordt een functie + een eigenschap aangeroepen in de foreach-constructie.

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

Dit is de equivalente expansie van het construct in C #. Je kunt je voorstellen hoe de prestatie-impact kan variëren op basis van de implementaties van Moventekst en Stroom. Overwegende dat u in een array-toegang die afhankelijkheden niet hebt.


6

Er is een ander interessant feit dat gemakkelijk kan worden gemist bij het testen van de snelheid van beide lussen:
Met behulp van de debug-modus laat de compiler de code niet optimaliseren met behulp van de standaardinstellingen.

Dit leidde me naar het interessante resultaat dat het foreach sneller is dan voor in de debug-modus. Overwegende dat het voor het is sneller is dan foreach in de releasemodus. Uiteraard heeft de compiler betere manieren om een ​​voor lus te optimaliseren dan een foreach-lus die verschillende methode-oproepen in gevaar brengt. A voor lus is trouwens zo fundamenteel dat dit mogelijk is dat dit zelfs wordt geoptimaliseerd door de CPU zelf.


7

In het voorbeeld dat u hebt opgegeven, is het zeker beter om foreachlus in plaats daarvan een forlus te gebruiken.

De standaard foreachConstruct kan sneller zijn (1,5 cycli per stap) dan een eenvoudige for-loop(2 cycli per stap), tenzij de lus is geweest Uitgerold (1,0 cycli per stap).

Dus voor dagelijkse code, prestaties zijn geen reden om de complexere for, whileof do-whileconstructen.

Bekijk deze link: 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               ║
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝


8

U kunt erover lezen in Diepe .NET – Deel 1 iteratie

Het is betrekking op de resultaten (zonder de eerste initialisatie) van .NET broncode helemaal naar de demontage.

Bijvoorbeeld – array iteratie met een foreach-lus:

en – lijst iteratie met foreach-lus:

en de eindresultaten:

Other episodes