C # 'for' ve 'foreach' kontrol yapıları için performans farkı


105

Hangi kod parçacığı daha iyi performans sağlar? Aşağıdaki kod segmentleri C # ile yazılmıştır.

1.

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

2.

foreach(MyType current in list)
{
    current.DoSomething();
}

31
Bunun gerçekten önemli olmadığını hayal ediyorum. Performans sorunları yaşıyorsanız, bunun nedeni neredeyse kesinlikle değildir. Soruyu
sormamalısın

2
Uygulamanız performans açısından çok kritik olmadığı sürece bu konuda endişelenmem. Temiz ve kolay anlaşılır bir koda sahip olmak çok daha iyi.
Fortyrunner

2
Buradaki bazı cevapların, beyninin herhangi bir yerinde bir yineleyici kavramına sahip olmayan ve dolayısıyla sıralayıcı veya işaretçi kavramı olmayan insanlar tarafından gönderilmiş gibi görünmesi beni endişelendiriyor.
Ed James

3
Bu 2. kod derlenmeyecek. System.Object, 'değer' adında bir üyeye sahip değildir (gerçekten kötü olmadıkça, onu bir uzantı yöntemi olarak tanımlamadıysanız ve temsilcileri karşılaştırmıyorsanız). Foreach'ınızı kesinlikle yazın.
Trillian

1
listGerçekten türünün countyerine bir üyesi olmadığı sürece, ilk kod da derlenmez Count.
Jon Skeet

Yanıtlar:


130

Kısmen tam olarak türüne bağlı list. Ayrıca, kullandığınız tam CLR'ye de bağlı olacaktır.

Herhangi bir şekilde önemli olup olmadığı, döngüde herhangi bir gerçek iş yapıp yapmamanıza bağlı olacaktır. Neredeyse tüm durumlarda, performans farkı önemli olmayacaktır, ancak okunabilirlik arasındaki fark foreachdöngüyü destekler .

Ben şahsen LINQ'i "eğer" den kaçınmak için kullanırım:

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

DÜZENLEME: Bir List<T>ile üzerinde yinelemenin döngü ile foreachaynı kodu ürettiğini iddia edenler için for, işte bunun olmadığına dair kanıt:

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

Şu IL'yi üretir:

.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

Derleyici dizileri farklı şekilde ele alır , bir foreachdöngüyü temelde bir döngüye dönüştürür for, ancak değil List<T>. İşte bir dizi için eşdeğer kod:

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

İlginç bir şekilde, bunu hiçbir yerde C # 3 şartnamesinde bulamıyorum ...


İlgisiz Jon, yukarıdaki List <T> senaryosu ... bu diğer koleksiyonlar için de geçerli mi? Ayrıca, bunu nasıl bildin (herhangi bir kötü niyet olmadan) ... olduğu gibi .. daha önce bir süre önce bu soruyu cevaplamaya çalışırken tam anlamıyla buna rastladınız mı? Bu çok ... rastgele / gizli :)
Pure.Krome

5
Dizi optimizasyonlarının bir süredir farkındayım - diziler "çekirdek" bir koleksiyon türüdür; C # derleyicisi zaten bunların derinlemesine farkındadır, bu nedenle onlara farklı şekilde davranması mantıklıdır. Derleyicinin hakkında özel bir bilgisi yoktur (ve olmamalıdır) List<T>.
Jon Skeet

Şerefe :) ve evet ... diziler yıllar ve yıllar önce üniversitede bana öğretilen ilk koleksiyon konseptiydi. Bu nedenle derleyicinin en ilkel türlerden biri ile (değilse de) baş edebilecek kadar akıllı olduğu anlaşılır. Toplamak. tekrar şerefe!
Pure.Krome

3
@JonSkeet Liste yineleyicisini optimize etmek, yineleme sırasında liste değiştirildiğinde davranışı değiştirir. Değiştirilirse istisnayı kaybedersiniz. Optimize etmek hala mümkündür, ancak hiçbir değişiklik yapılmadığının kontrol edilmesini gerektirir (diğer iş parçacıkları dahil, varsayıyorum).
Craig Gidney

5
@VeeKeyBee: 2004'te Microsoft dedi. A) işler değişir; b) işin önemli olması için her yinelemede küçük miktarlarda iş yapması gerekir. foreachBir dizi üzerinden foryine de eşdeğer olduğunu unutmayın . Her zaman önce okunabilirlik için kodlayın, ardından yalnızca ölçülebilir bir performans avantajı sağladığına dair kanıtınız olduğunda mikro optimizasyon yapın .
Jon Skeet

15

Bir fordöngü, buna yaklaşık olarak eşdeğer koda derlenir:

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

Bir foreachdöngü yaklaşık olarak buna eşdeğer koda derlendiğinde:

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

Gördüğünüz gibi, her şey numaralandırıcının nasıl uygulandığına karşı liste indeksleyicisinin nasıl uygulandığına bağlı olacaktır. Görünüşe göre dizilere dayalı türler için numaralandırıcı normalde şu şekilde yazılır:

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

Gördüğünüz gibi, bu durumda bu çok fazla fark yaratmayacak, ancak bağlantılı bir listenin numaralandırıcısı muhtemelen şuna benzer:

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

İçinde .NET veya bağlantılı bir listede bir döngü için yapmak mümkün olmaz böylece LinkedList <T> sınıfı bile, indexleyici yok olduğunu göreceksiniz; ancak yapabilseydiniz, dizin oluşturucunun şu şekilde yazılması gerekirdi:

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

Gördüğünüz gibi, bunu bir döngüde birden çok kez çağırmak, listenin neresinde olduğunu hatırlayabilen bir numaralandırıcı kullanmaktan çok daha yavaş olacaktır.


12

Yarı doğrulaması kolay bir test. Sadece görmek için küçük bir test yaptım. İşte 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();

}

Ve işte foreach bölümü:

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

For'u foreach ile değiştirdiğimde - foreach 20 milisaniye daha hızlıydı - tutarlı bir şekilde . For 135-139ms iken foreach 113-119ms idi. Birkaç kez ileri geri hareket ettim, sadece devreye giren bir süreç olmadığından emin oldum.

Ancak, foo ve if ifadesini kaldırdığımda, for 30 ms daha hızlıydı (foreach 88 ms ve for 59 ms idi). İkisi de boş kabuklardı. Foreach'in aslında bir değişkeni geçtiğini varsayıyorum, burada for sadece bir değişkeni artırıyordu. Eklersem

int foo = intList[i];

Sonra for, yaklaşık 30 ms yavaşlar. Bunun foo oluşturup dizideki değişkeni yakalayıp foo'ya atamakla ilgisi olduğunu varsayıyorum. Yalnızca intList [i] 'ye erişirseniz, o zaman bu cezaya sahip olmazsınız.

Dürüst olmak gerekirse .. foreach'in her koşulda biraz daha yavaş olmasını bekliyordum, ancak çoğu uygulamada önemli olacak kadar değil.

edit: işte Jons önerilerini kullanan yeni kod (134217728, System.OutOfMemory istisnası atılmadan önce sahip olabileceğiniz en büyük inttir):

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

Ve işte sonuçlar:

Veri üretiliyor. Döngü için hesaplama: 2458ms foreach döngü hesaplama: 2005ms

Her şeyin sırasını ele alıp almadığını görmek için onları değiştirmek aynı sonuçları verir (neredeyse).


6
Kronometreyi kullanmak DateTime'dan daha iyidir.Şimdi - ve dürüst olmak gerekirse bu kadar hızlı koşmaya güvenmem.
Jon Skeet

8
Foreach döngüleriniz daha hızlı çalışıyor çünkü 'for' her yinelemede koşulu değerlendiriyor. Örneğiniz durumunda, bu fazladan bir yöntem çağrısı yapar (list.count almak için) Kısacası, iki farklı kod parçasını karşılaştırıyorsunuz, dolayısıyla garip sonuçlarınız. 'İnt max = intlist.Count; for (int i = 0; i <max; i ++) ... 've' for 'döngüsü beklendiği gibi her zaman daha hızlı çalışacaktır!
AR

1
Derlemeden sonra, for ve foreach ilkellerle çalışırken tam olarak aynı şeyi optimize edin. Hız bakımından (büyük ölçüde) farklı oldukları List <T>'yi tanıtana kadar değil.
Anthony Russell

9

Not: Bu cevap Java için C # için olduğundan daha fazla geçerlidir, çünkü C # üzerinde bir indeksleyici yoktur LinkedLists, ancak genel noktanın hala geçerli olduğunu düşünüyorum.

Eğer birlikte listçalıştığınız kişi a LinkedListise, dizin oluşturucu kodunun performansı ( dizi tarzı erişim), büyük listeler için IEnumeratorfrom the kullanmaktan çok daha kötüdür foreach.

Bir LinkedListindeksleyici sözdizimini kullanarak 10.000 öğesine eriştiğinizde :, list[10000]bağlantılı liste baş düğümde başlayacak Nextve doğru nesneye ulaşana kadar-işaretleyiciyi on bin kez geçecektir. Açıkçası, bunu bir döngü içinde yaparsanız, şunları elde edersiniz:

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

Aradığınızda GetEnumerator(örtük kullanarak forach-Sözdizimi), bir alırsınız IEnumeratorkafa düğüme bir işaretçi olduğu nesneyi. Her çağırdığınızda MoveNext, bu işaretçi aşağıdaki gibi sonraki düğüme taşınır:

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.

Gördüğünüz gibi, LinkedLists durumunda , dizi indeksleyici yöntemi yavaşlar ve yavaşlar, ne kadar uzun döngü yaparsanız (aynı baş işaretçisinden defalarca geçmesi gerekir). Oysa IEnumerableadil sabit zamanda işler .

Tabii ki, Jon'un dediği gibi, bu gerçekten türüne bağlıdır list, eğer bir dizi listdeğilse LinkedList, davranış tamamen farklıdır.


4
.NET'te LinkedList bir indeksleyiciye sahip değildir, bu yüzden aslında bir seçenek değildir.
Jon Skeet

Oh, bu sorunu çözer, o zaman :-) LinkedList<T>MSDN üzerindeki dokümanlara bakıyorum ve oldukça iyi bir API'ye sahip. En önemlisi, get(int index)Java gibi bir yöntemi yoktur. Yine de, belirli bir indeksleyiciden daha yavaş bir indeksleyiciyi ortaya çıkaran diğer liste benzeri veri yapıları için hala geçerli olduğunu tahmin ediyorum IEnumerator.
Tom Lokhorst

2

Diğer insanların söylediği gibi performans aslında çok önemli olmasa da , döngüdeki IEnumerable/ IEnumeratorkullanımı nedeniyle foreach her zaman biraz daha yavaş olacaktır . Derleyici, yapıyı bu arabirimdeki çağrılara çevirir ve her adım için foreach yapısında bir işlev + bir özellik çağrılır.

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

Bu, yapının C # 'daki eşdeğer genişlemesidir. MoveNext ve Current uygulamalarına bağlı olarak performans etkisinin nasıl değişebileceğini hayal edebilirsiniz. Dizi erişiminde ise bu bağımlılıklara sahip değilsiniz.


4
Bir dizi erişimi ile bir dizinleyici erişimi arasında bir fark olduğunu unutmayın. Liste List<T>buradaysa, yine de indeksleyiciyi çağırma isabeti (muhtemelen satır içi) vardır. Çıplak metal dizi erişimi gibi değil.
Jon Skeet

Çok doğru! Yine başka bir mülkiyet uygulaması ve biz uygulamanın insafına kaldık.
Charles Prakash Dasari

1

"Okunabilirlik için foreach döngüsü tercih edilmelidir" şeklinde yeterince argüman okuduktan sonra, ilk tepkimin "ne" olduğunu söyleyebilirim? Okunabilirlik genel olarak özneldir ve bu özel durumda daha da fazladır. Programlamada geçmişi olan biri için (pratik olarak Java'dan önceki her dilde), for döngüleri foreach döngülerini okumaktan çok daha kolaydır. Ek olarak, foreach döngülerinin daha okunabilir olduğunu iddia eden aynı kişiler, kodun okunmasını ve sürdürülmesini zorlaştıran linq ve diğer "özelliklerin" destekçileridir, bu da yukarıdaki noktayı kanıtlar.

Performans üzerindeki etkisi hakkında, bu sorunun cevabına bakın .

DÜZENLEME: C # 'da indeksleyici olmayan koleksiyonlar (HashSet gibi) var. Bu koleksiyonlarında, foreach yineleme için tek yol olduğunu ve Bitsin kullanılması gerektiğini düşünüyorum sadece durumdur için .


0

Her iki döngünün hızını test ederken kolayca gözden kaçabilecek ilginç bir gerçek daha var: Hata ayıklama modunu kullanmak derleyicinin varsayılan ayarları kullanarak kodu optimize etmesine izin vermez.

Bu beni, foreach'in hata ayıklama modunda olduğundan daha hızlı olduğu gibi ilginç bir sonuca götürdü. For ist, serbest bırakma modunda foreach'den daha hızlıdır. Açıkçası, derleyicinin for döngüsünü optimize etmek için birkaç yöntem çağrısını tehlikeye atan bir foreach döngüsünden daha iyi yolları vardır. Bir for döngüsü öylesine temeldir ki, bunun CPU'nun kendisi tarafından bile optimize edilmesi mümkündür.


0

Sağladığınız örnekte, foreachdöngü yerine fordöngü kullanmak kesinlikle daha iyidir .

Döngü açılmadıkça (adım başına 1.0 döngü), standart foreachyapı basitten for-loop(adım başına 2 döngü ) daha hızlı olabilir (adım başına 1,5 döngü ).

Gündelik kodu Yani, performans daha karmaşık kullanmak için bir neden değildir for, whileya da do-whileyapıları.

Bu bağlantıya göz atın: 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               
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝


4
Bağladığınız kod projesi makalesini yeniden okuyabilirsiniz. İlginç bir makale, ancak yazınızın tam tersini söylüyor. Ayrıca, yeniden oluşturduğunuz tablo, bir diziye ve Listeye doğrudan veya IList arabirimleri aracılığıyla erişme performansını ölçüyor. Soruyla hiçbir ilgisi yok. :)
Paul Walls

0

Deep .NET - 1. Bölüm Yinelemesi'nde bununla ilgili okuyabilirsiniz

.NET kaynak kodundan sökmeye kadar sonuçları (ilk başlatma olmadan) kapsar.

örneğin - foreach döngüsüyle Dizi Yinelemesi: görüntü açıklamasını buraya girin

ve - foreach döngüsü ile yineleme listesi: görüntü açıklamasını buraya girin

ve son sonuçlar: görüntü açıklamasını buraya girin

görüntü açıklamasını buraya girin

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.