C# Performance - for vs. foreach?

4 Antworten

Von Experte FaTech bestätigt

Die for-Schleife ist in diesem Fall ein wenig schneller. Implizit wird sie in eine while-Schleife konvertiert und der Zugriff erfolgt über den Indexer.

Bei einer foreach-Schleife wird ein Iterator (konkret in C#: Enumerator) erstellt. Das sieht ungefähr so aus (den Typ habe ich einmal nur als T bezeichnet):

List<T>.Enumerator enumerator = list.GetEnumerator();

try
{
  while (enumerator.MoveNext())
  {
    T current = enumerator.Current;
    // ...
  }
}
finally
{
  ((IDisposable)enumerator).Dispose();
}

Würde es sich bei dem Kollektionstyp um ein Array handeln, würde zusätzlich noch eine neue Variable angelegt werden, die auf dasselbe Array-Objekt zeigt. Diese Variable wäre während der Iteration dann im Einsatz.

Nichtsdestotrotz fällt eine Entscheidung aus Performancegründen hier eher unter Mikrooptimierung. Ich würde dir stets raten, die lesbarere Variante zu wählen.

Gibt es sonst noch irgendwelche Unterschiede die man beachten sollte?

1) Mit Änderung des Datentyps, über den iteriert werden soll, kann sich natürlich auch einmal die Implementation ändern. Bei einer LinkedList beispielsweise müsstest du bei Gebrauch der for-Schleife den Kopf ändern:

for (LinkedListNode<string> node = projectiles.First; node != null; node = node.Next)

da dieser Typ keinen Zugriff via Indexer unterstützt. Die foreach-Schleife indes bleibt, wie sie ist.

Du solltest je Datentyp, mit dem du operieren möchtest, schauen, welche Schleife sich besser eignet.

2) Innerhalb der foreach-Schleife darfst du die Kollektion nicht ändern.

Beispiel:

var numbers = new List<int>() { 1, 2, 3 };

foreach (int number in numbers)
{
  numbers.Remove(number); // InvalidOperationException
}
mcmodderHD 
Fragesteller
 03.09.2023, 16:00

Danke für die ausführliche Erklärung. Tatsächlich fällt die Foreach Schleife bei mir raus, da Punkt 2 dagegen spricht. Ich muss innerhalb der Schleife die Möglichkeit haben ein Object aus der Liste zu entfernen.

Dann bleibt es wohl bei der normalen For Schleife.

0

Für dich in C# ist es nur wesentlich angenehmer einer Foreach-Schleife zu erstellen, als die Iteration von Hand zu tippen, wenn es nur darum geht im Einzelschritt eine Liste abzuarbeiten.

Der Weg ist bei beiden Schleifen am Ende aber der gleiche. Es werden 4 Variablen benötigt: der Zähler, der Schritt, die Unter- und Obergrenze.

Du kannst spaßeshalber einen IL-Reader nutzen und dir den IL-Code anschauen, der in deiner Executable entsteht. Da wirst du sehen dass der exakt gleiche Code bei beiden Schleifen herauskommt bzw. kommen sollte. Der Lerneffekt sich mal den IL-Code anzuschauen ist doch recht hoch. Das lohnt sich.

Fazit: Nein es gibt keinen Performance-Unterschied zwischen Foreach und For -Schleifen, solange ihr Ablauf gleich ist.

LG Knom

Woher ich das weiß:Studium / Ausbildung – Softwareentwickler mit 10 Jahren Berufserfahrung 💾
regex9  03.09.2023, 16:25
Du kannst spaßeshalber einen IL-Reader nutzen und dir den IL-Code anschauen, der in deiner Executable entsteht

Für:

class T
{
  public void Update()
  {
  }
}

class Program
{
  static void Main(string[] args)
  {
    List<T> projectiles = new List<T>();

    for (int i = 0; i < projectiles.Count; i++)
    {
      projectiles[i].Update();
    }

    foreach (T item in projectiles)
    {
      item.Update();
    }

    Console.ReadKey();
}

würde die Main-Methode so aussehen (die Zeilen mit # sind von mir ergänzte Kommentare):

.method private hidebysig static void Main (string[] args) cil managed
{
  .entrypoint

  # declaration of local variables
  .locals init (
    [0] class [mscorlib]System.Collections.Generic.List`1<class CsharpConsoleTests.T> projectiles,
    [1] int32 i,
    [2] bool V_2,
    [3] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class CsharpConsoleTests.T> V_3,
    [4] class CsharpConsoleTests.T item
  )

  # create list
  IL_0000: nop
  IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<class CsharpConsoleTests.T>::.ctor()

  # for loop
  IL_0006: stloc.0
  IL_0007: ldc.i4.0
  IL_0008: stloc.1
  IL_0009: br.s IL_001e

  .loop {
    IL_000b: nop
    IL_000c: ldloc.0
    IL_000d: ldloc.1
    IL_000e: callvirt instance class CsharpConsoleTests.T class [mscorlib]System.Collections.Generic.List`1<class CsharpConsoleTests.T>::get_Item(int32)
    IL_0013: callvirt instance void CsharpConsoleTests.T::Update()
    IL_0018: nop
    IL_0019: nop
    IL_001a: ldloc.1
    IL_001b: ldc.i4.1
    IL_001c: add
    IL_001d: stloc.1

    IL_001e: ldloc.1
    IL_001f: ldloc.0
    IL_0020: callvirt instance int32 class [mscorlib]System.Collections.Generic.List`1<class CsharpConsoleTests.T>::get_Count()
    IL_0025: clt
    IL_0027: stloc.2
    IL_0028: ldloc.2
    IL_0029: brtrue.s IL_000b
  }

  # foreach loop (create enumerator, try-finally, loop itself)
  IL_002b: nop
  IL_002c: ldloc.0
  IL_002d: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class CsharpConsoleTests.T> class [mscorlib]System.Collections.Generic.List`1<class CsharpConsoleTests.T>::GetEnumerator()
  IL_0032: stloc.3

  .try {
    IL_0033: br.s IL_0048
    .loop {
      IL_0035: ldloca.s V_3
      IL_0037: call instance class CsharpConsoleTests.T valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class CsharpConsoleTests.T>::get_Current()
      IL_003c: stloc.s item
      IL_003e: nop
      IL_003f: ldloc.s item
      IL_0041: callvirt instance void CsharpConsoleTests.T::Update()
      IL_0046: nop
      IL_0047: nop
      IL_0048: ldloca.s V_3

      IL_004a: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class CsharpConsoleTests.T>::MoveNext()
      IL_004f: brtrue.s IL_0035
    }

    IL_0051: leave.s IL_0062
  }
  finally {
    IL_0053: ldloca.s V_3
    IL_0055: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class CsharpConsoleTests.T>
    IL_005b: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    IL_0060: nop
    IL_0061: endfinally
  }

  # method end with Console.ReadKey
  IL_0062: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
  IL_0067: pop
  IL_0068: ret
}
0

Wenn der Compiler gut ist, dann würde der das beides gleich behandeln. Weil er bei beiden Versionen die Referenz braucht

Woher ich das weiß:Studium / Ausbildung – Aktives Studium: Informatik Technischer Systeme
mcmodderHD 
Fragesteller
 03.09.2023, 15:04

Und wie verhält sich der Code, wenn man ein Item aus der List entfernt?

Ich weiß dass man in der for-Loop die Zählervariable dekrementieren muss. Aber bei der Foreach bin ich mir nicht sicher ob man dort irgendwas machen muss.

for(int i = 0; i < projectiles.Count; i++)
{
  projectiles[i].Update();

  if (projectiles[i].done)
  {
    projectiles.RemoveAt(i);
    i--;
  }
}

foreach(Projectile2D item in projectiles)
{
  item.Update();

  if (item.done)
  {
    projectiles.Remove(item);
  }
}
      
0
TheStalker64  03.09.2023, 15:07
@mcmodderHD

Sowas ist sehr böse. Glaub die 2. Variante schlägt sogar fehlt. Dafür musst du den iterator direkt benutzen

Sauberer ist es eine 2. Liste zu nutzen dafür oder ahnliches

Und bei deinem neuen snippet ist done immer direkt nach Update gesetzt? Wenn nicht wäre eine while Schleife vermutlich sinnvoller oder?

1
mcmodderHD 
Fragesteller
 03.09.2023, 15:54
@TheStalker64

Habe gerade gemerkt, ja die zweite Variante schlägt fehl. "Collection was modified; enumeration operation may not execute."

done wird in der Update Function von Projectile2D gesetzt anhand eines Timers.

// Constructor:
timer = new Timer(1200);

// Update:
if (timer.IsDone())
{
  done = true;
}

// Timer:
public bool IsDone()
{
  if (timer.TotalMilliseconds >= mSec || timerStarted)
    return true;
  else
    return false;
}
1

Du hast Recht, dass die `foreach`-Variante oft einfacher zu lesen ist. In den meisten Fällen ist der Performance-Unterschied zwischen `for` und `foreach` minimal und kaum bemerkbar.

Ein paar Dinge, die du beachten solltest:

1. Bei der `foreach`-Schleife wird intern ein Enumerator verwendet, was ein klein wenig zusätzlichen Overhead bedeuten könnte.

2. `foreach` ist sicherer, da du die Kollektion nicht direkt manipulierst. Mit `for` könntest du versehentlich Elemente hinzufügen oder entfernen und damit Probleme verursachen.

3. Mit `for` hast du mehr Kontrolle, etwa über den Index oder die Schrittweite. Das ist bei komplexeren Logiken manchmal nützlich.

4. `foreach` kann nicht nur auf Listen, sondern auf allen Kollektionen arbeiten, die das `IEnumerable`-Interface implementieren.

Im Allgemeinen, wenn du keine speziellen Anforderungen hast, ist `foreach` für die Lesbarkeit oft besser. Wenn es aber auf Performance ankommt (z.B. in einer Game-Loop), könnte `for` die bessere Wahl sein.

Woher ich das weiß:eigene Erfahrung
mcmodderHD 
Fragesteller
 03.09.2023, 15:56

Habe festgestellt dass Foreach meine Anforderungen nicht erfüllt, da ich Elemente eventuell aus der Liste entfernen muss während die Loop läuft.

1
Charmin  03.09.2023, 16:00
@mcmodderHD

Ah, das ist ein wichtiger Punkt. In der Tat, wenn du Elemente während der Iteration hinzufügen oder entfernen willst, ist `foreach` nicht geeignet, da die Kollektion während der Iteration nicht verändert werden darf. In diesem Fall ist die `for`-Schleife die bessere Wahl, da sie dir die Flexibilität gibt, die Liste sicher zu modifizieren, solange du dabei den Index und die Länge der Liste sorgfältig handhabst.

1