Różnica w wydajności struktur kontrolnych „for” i „foreach” w języku C #

105

Który fragment kodu zapewni lepszą wydajność? Poniższe segmenty kodu zostały napisane w języku C #.

1.

for(int counter=0; counter<list.Count; counter++)
{
    list[counter].DoSomething();
}

2.

foreach(MyType current in list)
{
    current.DoSomething();
}
Kthevar
źródło
31
Wyobrażam sobie, że to nie ma znaczenia. Jeśli masz problemy z wydajnością, prawie na pewno nie jest to spowodowane tym. Nie żebyś nie zadawał tego pytania ...
darasd
2
Nie martwiłbym się tym, chyba że Twoja aplikacja ma duże znaczenie dla wydajności. O wiele lepiej mieć czysty i łatwy do zrozumienia kod.
Fortyrunner
2
Martwi mnie, że niektóre odpowiedzi tutaj wydają się być publikowane przez ludzi, którzy po prostu nie mają pojęcia iteratora nigdzie w swoim mózgu, a zatem nie mają pojęcia wyliczających ani wskaźników.
Ed James,
3
Ten drugi kod się nie skompiluje. System.Object nie ma elementu członkowskiego o nazwie „wartość” (chyba że jesteś naprawdę zły, zdefiniowałeś go jako metodę rozszerzającą i porównujesz delegatów). Mocno wpisz swoje foreach.
Trillian
1
Pierwszy kod również się nie skompiluje, chyba że typ listnaprawdę ma countzamiast elementu składowego Count.
Jon Skeet,

Odpowiedzi:

130

Cóż, częściowo zależy to od dokładnego typu list. Będzie to również zależeć od dokładnego CLR, którego używasz.

To, czy jest to w jakiś sposób znaczące, czy nie, będzie zależeć od tego, czy wykonujesz jakąkolwiek prawdziwą pracę w pętli. W prawie wszystkich przypadkach różnica w wydajności nie będzie znacząca, ale różnica w czytelności sprzyja foreachpętli.

Osobiście użyłbym LINQ, aby również uniknąć „jeśli”:

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

EDIT: Dla tych z Was, którzy twierdzą, że iteracja ponad List<T>z foreachprodukuje ten sam kod jak forpętla, oto dowód, że tak nie jest:

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

Produkuje IL:

.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

Kompilator inaczej traktuje tablice , konwertując plikforeach pętlę w zasadzie na forpętlę, ale nie List<T>. Oto równoważny kod dla tablicy:

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

Co ciekawe, nigdzie nie mogę znaleźć tego udokumentowanego w specyfikacji C # 3 ...

Jon Skeet
źródło
Interesujący Jon, scenariusz z List <T> powyżej ... czy to dotyczy również innych kolekcji? Poza tym, skąd o tym wiedziałeś (bez jakiejkolwiek złośliwości)… jak w… czy dosłownie natknąłeś się na to, próbując odpowiedzieć na to pytanie, wcześniej jakiś czas temu? To takie ... losowe / sekretne :)
Pure.Krome
5
Od jakiegoś czasu jestem świadomy optymalizacji tablic - tablice to zbiór „podstawowych”; kompilator C # jest już w pełni świadomy ich istnienia, dlatego warto je traktować inaczej. Kompilator nie ma (i nie powinien) mieć żadnej specjalnej wiedzy na temat List<T>.
Jon Skeet,
Pozdrawiam :) i tak ... tablice były pierwszą koncepcją kolekcji, której nauczyłem się wiele lat temu na uniwersytecie, więc byłoby sensem, że kompilator jest wystarczająco inteligentny, aby poradzić sobie z jednym z (jeśli nie) najbardziej prymitywnym typem kolekcja. okrzyki znowu!
Pure.Krome,
3
@JonSkeet Optymalizacja iteratora listy zmienia zachowanie, gdy lista jest modyfikowana podczas iteracji. Tracisz wyjątek, jeśli zmodyfikowano. Nadal jest możliwa optymalizacja, ale wymaga sprawdzenia, czy nie zachodzą żadne modyfikacje (zakładam, w tym w innych wątkach).
Craig Gidney
5
@VeeKeyBee: Tak powiedział Microsoft w 2004 roku. A) rzeczy się zmieniają; b) praca wymagałaby wykonania niewielkiej ilości pracy przy każdej iteracji, aby było to znaczące. Zauważ, że foreachnad tablicą jest równoważne fortak czy inaczej. Zawsze najpierw koduj pod kątem czytelności, a dopiero potem mikrooptymalizuj, jeśli masz dowody , że daje to wymierne korzyści w zakresie wydajności.
Jon Skeet
15

forPętla zostanie skompilowany do kodu w przybliżeniu równoważne to:

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

Gdzie jako foreachpętla jest kompilowana do kodu w przybliżeniu równoważnego z tym:

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

Jak widać, wszystko zależałoby od tego, w jaki sposób moduł wyliczający jest zaimplementowany, a jak zaimplementowany jest indeksator list. Jak się okazuje, moduł wyliczający dla typów opartych na tablicach jest zwykle zapisywany mniej więcej tak:

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

Jak więc widać, w tym przypadku nie zrobi to dużej różnicy, jednak moduł wyliczający dla połączonej listy prawdopodobnie wyglądałby mniej więcej tak:

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

W .NET zauważysz, że klasa LinkedList <T> nie ma nawet indeksatora, więc nie byłbyś w stanie wykonać pętli for na połączonej liście; ale gdybyś mógł, indeksator musiałby być napisany w ten sposób:

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

Jak widać, wielokrotne wywoływanie tego w pętli będzie znacznie wolniejsze niż użycie modułu wyliczającego, który zapamięta, gdzie się znajduje na liście.

Martin Brown
źródło
12

Łatwy test do pół-walidacji. Zrobiłem mały test, żeby zobaczyć. Oto kod:

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

}

A oto sekcja dla wszystkich:

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

Kiedy wymieniłem for na foreach - foreach był o 20 milisekund szybszy - konsekwentnie . Dla było 135-139 ms, podczas gdy foreach było 113-119 ms. Kilka razy zamieniałem się tam iz powrotem, upewniając się, że to nie jakiś proces właśnie się rozpoczął.

Jednak gdy usunąłem instrukcję foo i if, for było szybsze o 30 ms (foreach to 88 ms, a for było 59 ms). Obie były pustymi muszlami. Zakładam, że foreach faktycznie przekazał zmienną, gdzie jako for po prostu zwiększał zmienną. Jeśli dodałem

int foo = intList[i];

Następnie for stają się wolne o około 30 ms. Zakładam, że miało to związek z utworzeniem foo i pobraniem zmiennej w tablicy i przypisaniem jej do foo. Jeśli uzyskasz dostęp do intList [i], nie będziesz mieć tej kary.

Szczerze mówiąc ... Spodziewałem się, że foreach będzie nieco wolniejsze we wszystkich okolicznościach, ale nie na tyle, aby miało to znaczenie w większości zastosowań.

edit: oto nowy kod wykorzystujący sugestie Jonsa (134217728 to największe int, jakie możesz mieć przed wyrzuceniem wyjątku System.OutOfMemory):

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

A oto wyniki:

Generowanie danych. Obliczanie dla pętli: 2458 ms Obliczanie każdej pętli: 2005 ms

Zamiana ich miejsc, aby zobaczyć, czy zajmuje się kolejnością rzeczy, daje takie same wyniki (prawie).

Kenny Mann
źródło
6
Lepiej jest używać Stopwatch niż DateTime. Teraz - i szczerze mówiąc, nie ufałbym żadnemu tak szybkiemu biegowi.
Jon Skeet,
8
Twoje pętle foreach działają szybciej, ponieważ „for” ocenia warunek w każdej iteracji. W przypadku twojego przykładu powoduje to jedno dodatkowe wywołanie metody (aby uzyskać list.count) Krótko mówiąc, porównujesz dwa różne fragmenty kodu, stąd twoje dziwne wyniki. Spróbuj 'int max = intlist.Count; for (int i = 0; i <max; i ++) ... 'i pętla' for 'zawsze będą działać szybciej, zgodnie z oczekiwaniami!
AR
1
Po kompilacji optymalizuj for i foreach, aby uzyskać dokładnie to samo podczas pracy z prymitywami. Dopóki nie wprowadzisz List <T>, różnią się one (znacznie) szybkością.
Anthony Russell
9

Uwaga: ta odpowiedź dotyczy bardziej języka Java niż języka C #, ponieważ C # nie ma włączonego indeksatora LinkedLists , ale myślę, że ogólny punkt jest nadal aktualny.

Jeśli listakurat to LinkedList, z którym pracujesz , to wydajność kodu indeksującego ( dostęp w stylu tablicy ) jest o wiele gorsza niż przy użyciu IEnumeratorz foreach, dla dużych list.

Po uzyskaniu dostępu do elementu 10.000 w a LinkedListprzy użyciu składni indeksatora :, list[10000]połączona lista rozpocznie się w węźle głównym i Nextprzejdzie przez -pointer dziesięć tysięcy razy, aż dotrze do właściwego obiektu. Oczywiście, jeśli zrobisz to w pętli, otrzymasz:

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

Kiedy wywołujesz GetEnumerator(niejawnie używając forach-syntax), otrzymasz IEnumeratorobiekt, który ma wskaźnik do węzła głównego. Za każdym razem, gdy dzwonisz MoveNext, ten wskaźnik jest przenoszony do następnego węzła, na przykład:

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.

Jak widać, w przypadku LinkedLists metoda indeksowania tablicy staje się coraz wolniejsza, im dłuższa jest pętla (musi w kółko przechodzić przez ten sam wskaźnik głowicy). Natomiast IEnumerablesprawiedliwy działa w ciągłym czasie.

Oczywiście, jak powiedział Jon, to naprawdę zależy od typu list, jeśli listnie jest LinkedListto tablica, ale tablica, zachowanie jest zupełnie inne.

Tom Lokhorst
źródło
4
LinkedList w .NET nie ma indeksatora, więc w rzeczywistości nie jest to opcja.
Jon Skeet,
No cóż, to rozwiązuje ten problem :-) Po prostu przeglądam LinkedList<T>dokumentację na MSDN i ma całkiem przyzwoity interfejs API. Co najważniejsze, nie ma get(int index)metody, takiej jak Java. Mimo to wydaje mi się, że ten punkt nadal dotyczy każdej innej struktury danych podobnej do listy, która ujawnia indeksator, który jest wolniejszy niż konkretny IEnumerator.
Tom Lokhorst
2

Jak wspominali inni ludzie, chociaż wydajność w rzeczywistości nie ma większego znaczenia, foreach zawsze będzie trochę wolniejsze z powodu użycia IEnumerable/ IEnumeratorw pętli. Kompilator tłumaczy konstrukcję na wywołania tego interfejsu i dla każdego kroku w konstrukcji foreach wywoływana jest funkcja + właściwość.

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

To jest równoważne rozwinięcie konstrukcji w C #. Możesz sobie wyobrazić, jak wpływ na wydajność może się różnić w zależności od implementacji MoveNext i Current. Podczas gdy w dostępie do tablicy nie masz takich zależności.

Charles Prakash Dasari
źródło
4
Nie zapominaj, że istnieje różnica między dostępem do tablicy a dostępem do indeksatora. Jeśli lista jest List<T>tutaj, nadal istnieje trafienie (prawdopodobnie wbudowane) w wywołanie indeksatora. To nie jest tak, że jest to dostęp do gołej metalowej tablicy.
Jon Skeet,
Bardzo prawdziwe! To kolejna egzekucja majątkowa, a my jesteśmy zdani na realizację.
Charles Prakash Dasari
1

Po przeczytaniu wystarczającej liczby argumentów, że „pętla foreach powinna być preferowana ze względu na czytelność”, mogę powiedzieć, że moją pierwszą reakcją było „co”? Czytelność jest ogólnie rzecz biorąc subiektywna, aw tym konkretnym przypadku nawet bardziej. Dla kogoś, kto ma doświadczenie w programowaniu (praktycznie każdy język przed Javą), pętle for są znacznie łatwiejsze do odczytania niż pętle foreach. Ponadto ci sami ludzie, którzy twierdzą, że pętle foreach są bardziej czytelne, są też zwolennikami linq i innych „funkcji”, które utrudniają odczyt i konserwację kodu, co potwierdza powyższy punkt.

O wpływie na wydajność zobacz odpowiedź na to pytanie.

EDYCJA: istnieją kolekcje w C # (takie jak HashSet), które nie mają indeksatora. W tych zbiorów, foreach jest jedyną drogą do iteracji i jest to jedyny przypadek, myślę, że powinno być stosowane na na .

ThunderGr
źródło
0

Jest jeszcze jeden interesujący fakt, który można łatwo przeoczyć podczas testowania szybkości obu pętli: użycie trybu debugowania nie pozwala kompilatorowi na optymalizację kodu przy użyciu ustawień domyślnych.

To doprowadziło mnie do interesującego wyniku, że foreach jest szybszy niż w trybie debugowania. Podczas gdy w trybie zwalniania for jest szybszy niż każdy inny. Oczywiście kompilator ma lepsze sposoby na optymalizację pętli for niż pętla foreach, która narusza kilka wywołań metod. Nawiasem mówiąc, pętla for jest tak fundamentalna, że ​​możliwe, że jest nawet optymalizowana przez sam procesor.

Sam
źródło
0

W podanym przykładzie zdecydowanie lepiej jest użyć foreachpętli zamiast forpętli.

Standardowa foreachkonstrukcja może być szybsza (1,5 cykli na krok) niż prosta for-loop(2 cykle na krok), chyba że pętla została rozwinięta (1,0 cykli na krok).

Tak dla kodu codziennym, wydajność nie jest powodem do korzystania z bardziej złożonych for, whilelub do-whilekonstrukcje.

Sprawdź ten 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               
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝

rpax
źródło
4
Możesz ponownie przeczytać artykuł dotyczący projektu kodu, który został połączony. To interesujący artykuł, ale mówi dokładnie odwrotnie niż twój post. Ponadto utworzona ponownie tabela mierzy wydajność dostępu do tablicy i listy bezpośrednio lub przez ich interfejsy IList. Ani jedno, ani drugie nie ma nic wspólnego z tym pytaniem. :)
Paul Walls
0

możesz o tym przeczytać w Deep .NET - część 1 Iteracja

obejmuje wyniki (bez pierwszej inicjalizacji) z kodu źródłowego .NET aż do dezasemblacji.

na przykład - Array Iteration with a foreach loop: wprowadź opis obrazu tutaj

i - iteracja listy z pętlą foreach: wprowadź opis obrazu tutaj

a efekty końcowe: wprowadź opis obrazu tutaj

wprowadź opis obrazu tutaj

Albo Yaacov
źródło