105 Stimmen

Leistungsunterschiede bei den Kontrollstrukturen 'for' und 'foreach' in C#

Welcher Codeschnipsel bringt eine bessere Leistung? Die folgenden Codesegmente wurden in C# geschrieben.

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

130voto

Jon Skeet Punkte 1325502

Nun, das hängt zum Teil von der genauen Art der list . Es hängt auch von der genauen CLR ab, die Sie verwenden.

Ob es in irgendeiner Weise bedeutsam oder nicht, hängt davon ab, ob Sie wirklich in der Schleife arbeiten. In fast alle Fällen ist der Unterschied in der Leistung nicht signifikant, aber der Unterschied in der Lesbarkeit begünstigt die foreach Schleife.

Ich persönlich würde LINQ verwenden, um das "if" zu vermeiden:

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

EDIT: Für diejenigen unter Ihnen, die behaupten, dass die Iteration über eine List<T> mit foreach erzeugt den gleichen Code wie der for Schleife, hier ist der Beweis, dass das nicht der Fall ist:

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

Erzeugt IL von:

.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

Der Compiler behandelt Arrays anders, die Umwandlung einer foreach Schleife grundsätzlich zu einer for Schleife, aber nicht List<T> . Hier ist der entsprechende Code für ein 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

Interessanterweise kann ich dies in der C# 3 Spezifikation nirgends dokumentiert finden...

14voto

Martin Brown Punkte 23597

A for Schleife wird zu einem Code kompiliert, der in etwa dem folgenden entspricht:

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

Während ein foreach Schleife wird zu einem Code kompiliert, der in etwa dem folgenden entspricht:

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

Wie Sie also sehen können, hängt alles davon ab, wie der Enumerator implementiert ist und wie der Listenindexer implementiert ist. Wie sich herausstellt, wird der Enumerator für Typen, die auf Arrays basieren, normalerweise wie folgt geschrieben:

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

Wie Sie also sehen können, macht es in diesem Fall keinen großen Unterschied, aber der Enumerator für eine verknüpfte Liste würde wahrscheinlich so aussehen:

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

Unter .NET werden Sie feststellen, dass die Klasse LinkedList<T> nicht einmal einen Indexer hat, so dass Sie Ihre for-Schleife nicht auf einer verknüpften Liste ausführen können; aber wenn Sie das könnten, müsste der Indexer so geschrieben werden:

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

Wie Sie sehen können, wird der mehrfache Aufruf in einer Schleife viel langsamer sein als die Verwendung eines Aufzählungszeichens, das sich merken kann, wo es sich in der Liste befindet.

12voto

Kenny Mann Punkte 881

Ein einfacher Test zur Halbvalidierung. Ich habe einen kleinen Test gemacht, nur um zu sehen. Hier ist der 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();

}

Und hier ist der foreach-Abschnitt:

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

Als ich das for durch ein foreach ersetzt habe, war das foreach 20 Millisekunden schneller. durchgehend . Das for war 135-139ms, während das foreach 113-119ms war. Ich habe mehrmals hin- und hergeschaltet, um sicherzustellen, dass es sich nicht um einen Prozess handelt, der gerade erst begonnen hat.

Als ich jedoch das foo und die if-Anweisung entfernte, war das for um 30 ms schneller (foreach war 88 ms und for war 59 ms). Es waren beides leere Shells. Ich nehme an, dass foreach tatsächlich eine Variable übergeben hat, während for nur eine Variable inkrementiert hat. Wenn ich Folgendes hinzufüge

int foo = intList[i];

Dann werden die für um etwa 30ms langsamer. Ich nehme an, dass dies mit der Erstellung von foo und dem Zugriff auf die Variable im Array und deren Zuweisung an foo zu tun hat. Wenn Sie nur intList[i] zugreifen, dann haben Sie diese Strafe nicht.

Um ehrlich zu sein Ich habe erwartet, dass foreach unter allen Umständen etwas langsamer ist, aber nicht genug, um in den meisten Anwendungen eine Rolle zu spielen.

Bearbeiten: hier ist der neue Code mit Jons Vorschläge (134217728 ist die größte int können Sie haben, bevor System.OutOfMemory Ausnahme ausgelöst wird):

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

Und hier sind die Ergebnisse:

Generierung von Daten. Berechnung der for-Schleife: 2458ms Berechnen der foreach-Schleife: 2005ms

Wenn man sie vertauscht, um zu sehen, ob es mit der Reihenfolge der Dinge zu tun hat, erhält man (fast) die gleichen Ergebnisse.

9voto

Tom Lokhorst Punkte 13200

Hinweis: Diese Antwort gilt mehr für Java als für C#, da C# keinen Indexer auf LinkedLists aber ich denke, die allgemeine Aussage ist immer noch gültig.

Wenn die list mit dem Sie arbeiten, ist zufällig ein LinkedList die Leistung des Indexer-Codes ( Array-Stil Zugriff) ist viel schlechter als die Verwendung des IEnumerator de la foreach für große Listen.

Wenn Sie auf das Element 10.000 in einer LinkedList unter Verwendung der Indexer-Syntax: list[10000] beginnt die verknüpfte Liste am Kopfknoten und durchläuft die Next -Zeiger zehntausendmal, bis er das richtige Objekt erreicht. Wenn Sie dies in einer Schleife tun, erhalten Sie natürlich:

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

Wenn Sie anrufen GetEnumerator (unter impliziter Verwendung der forach -Syntax), erhalten Sie eine IEnumerator Objekt, das einen Zeiger auf den Kopfknoten enthält. Jedes Mal, wenn Sie MoveNext wird dieser Zeiger auf den nächsten Knoten verschoben, etwa so:

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.

Wie Sie sehen können, ist im Fall von LinkedList s wird die Array-Indexer-Methode immer langsamer, je länger die Schleife dauert (sie muss denselben Kopfzeiger immer wieder durchlaufen). Während die IEnumerable arbeitet einfach in konstanter Zeit.

Natürlich hängt dies, wie Jon schon sagte, von der Art der list wenn die list ist keine LinkedList , sondern ein Array, ist das Verhalten völlig anders.

2voto

Wie andere Leute erwähnt haben, obwohl die Leistung nicht wirklich viel ausmacht, wird die foreach immer ein wenig langsamer sein, weil der IEnumerable / IEnumerator Verwendung in der Schleife. Der Compiler übersetzt das Konstrukt in Aufrufe auf dieser Schnittstelle und für jeden Schritt werden eine Funktion und eine Eigenschaft im foreach-Konstrukt aufgerufen.

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

Dies ist die entsprechende Erweiterung des Konstrukts in C#. Sie können sich vorstellen, wie die Auswirkungen auf die Leistung je nach den Implementierungen von MoveNext und Current variieren können. Bei einem Array-Zugriff hingegen gibt es diese Abhängigkeiten nicht.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X