C# Performance - for vs. foreach?
Gibt es einen Unterschied zwischen den unterschiedlichen Verarbeitungen der beiden Schleifen?
List<Projectile2D> projectiles = new List<Projectile2D>();
(...)
for(int i = 0; i < projectiles.Count; i++)
{
projectiles[i].Update();
}
foreach(Projectile2D item in projectiles)
{
item.Update();
}
Ich könnte mir höchstens vorstellen, dass die zweite Variante ein klein wenig mehr RAM verbraucht, da hier ja eine neue Variable instanziert wird.
Gibt es sonst noch irgendwelche Unterschiede die man beachten sollte? Die meisten Tutorials zeigen immer die obere Variante. Ich finde jedoch, dass die untere viel besser zu verstehen ist.
4 Antworten
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
}
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.
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
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
}
Wenn der Compiler gut ist, dann würde der das beides gleich behandeln. Weil er bei beiden Versionen die Referenz braucht
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?
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;
}
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.
Habe festgestellt dass Foreach meine Anforderungen nicht erfüllt, da ich Elemente eventuell aus der Liste entfernen muss während die Loop läuft.
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.
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.