Perbedaan kinerja untuk struktur kontrol 'for' dan 'foreach' di C #

105

Potongan kode mana yang akan memberikan kinerja yang lebih baik? Segmen kode di bawah ini ditulis dalam C #.

1.

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

2.

foreach(MyType current in list)
{
    current.DoSomething();
}
Kthevar
sumber
31
Saya membayangkan bahwa itu tidak terlalu penting. Jika Anda mengalami masalah kinerja, hampir pasti bukan karena ini. Bukan berarti Anda tidak harus mengajukan pertanyaan ...
darasd
2
Kecuali jika aplikasi Anda sangat penting dalam performa, saya tidak akan mengkhawatirkan hal ini. Jauh lebih baik untuk memiliki kode yang bersih dan mudah dimengerti.
Fortyrunner
2
Saya khawatir bahwa beberapa jawaban di sini tampaknya diposting oleh orang-orang yang sama sekali tidak memiliki konsep iterator di mana pun di otak mereka, dan oleh karena itu tidak ada konsep enumerator atau petunjuk.
Ed James
3
Kode kedua itu tidak dapat dikompilasi. System.Object tidak memiliki anggota yang disebut 'nilai' (kecuali jika Anda benar-benar jahat, telah mendefinisikannya sebagai metode ekstensi dan membandingkan delegasi). Ketikkan foreach Anda dengan kuat.
Trillian
1
Kode pertama juga tidak dapat dikompilasi, kecuali tipe listbenar - benar memiliki anggota count, bukan Count.
Jon Skeet

Jawaban:

130

Yah, sebagian tergantung pada jenis pastinya list. Ini juga akan tergantung pada CLR persis yang Anda gunakan.

Apakah itu signifikan atau tidak, akan tergantung pada apakah Anda melakukan pekerjaan nyata dalam loop. Di hampir semua kasus, perbedaan kinerja tidak akan signifikan, tetapi perbedaan pada keterbacaan mendukung foreachloop.

Saya pribadi menggunakan LINQ untuk menghindari "jika" juga:

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

EDIT: Bagi Anda yang mengklaim bahwa iterasi List<T>dengan a foreachmenghasilkan kode yang sama dengan forloop, inilah bukti bahwa itu tidak:

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

Menghasilkan IL dari:

.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

Kompiler memperlakukan array secara berbeda, foreachpada dasarnya mengubah sebuah loop menjadi sebuah forloop, tetapi tidak List<T>. Berikut kode yang setara untuk sebuah 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

Menariknya, saya tidak dapat menemukan ini didokumentasikan dalam spesifikasi C # 3 di mana pun ...

Jon Skeet
sumber
Yang menarik, Jon, skenario dengan List <T> di atas ... apakah itu juga berlaku untuk koleksi lain? Juga, bagaimana Anda tahu ini (tanpa niat jahat apa pun) ... seperti dalam .. apakah Anda benar-benar tersandung saat mencoba menjawab pertanyaan ini, sebelumnya beberapa waktu yang lalu? Ini sangat ... acak / rahasia :)
Pure.Krome
5
Saya telah mengetahui optimisasi array untuk sementara waktu - array adalah jenis koleksi "inti"; kompilator C # sudah sangat memahaminya, jadi masuk akal untuk memperlakukannya secara berbeda. Kompilator tidak (dan tidak seharusnya) memiliki pengetahuan khusus tentang List<T>.
Jon Skeet
Cheers :) dan ya ... array adalah konsep koleksi pertama yang saya ajarkan bertahun-tahun yang lalu di universitas .. jadi akan masuk akal bahwa kompilator cukup pintar untuk menangani salah satu (jika bukan) jenis yang paling primitif dari koleksi. bersorak lagi!
Pure.Krome
3
@JonSkeet Mengoptimalkan daftar iterator pergi mengubah perilaku ketika daftar diubah selama iterasi. Anda kehilangan pengecualian-jika-diubah. Ini masih mungkin untuk dioptimalkan, tetapi memerlukan pemeriksaan bahwa tidak ada modifikasi yang terjadi (termasuk pada utas lain, saya asumsikan).
Craig Gidney
5
@VeeKeyBee: Demikian kata Microsoft pada tahun 2004. a) banyak hal berubah; b) pekerjaan akan harus melakukan kecil jumlah pekerjaan pada setiap iterasi untuk ini menjadi signifikan. Perhatikan bahwa foreachover array setara dengan foranyway. Selalu buat kode untuk keterbacaan terlebih dahulu, lalu hanya optimalkan mikro jika Anda memiliki bukti bahwa hal itu memberikan manfaat kinerja yang terukur.
Jon Skeet
15

Sebuah forloop dikompilasi ke kode yang kira-kira setara dengan ini:

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

Dimana sebuah foreachloop dikompilasi ke kode yang kira-kira setara dengan ini:

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

Jadi seperti yang Anda lihat, itu semua akan tergantung pada bagaimana enumerator diimplementasikan versus bagaimana pengindeks daftar diimplementasikan. Ternyata pencacah untuk tipe berdasarkan array biasanya ditulis seperti ini:

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

Jadi seperti yang Anda lihat, dalam hal ini tidak akan membuat banyak perbedaan, namun enumerator untuk daftar tertaut mungkin akan terlihat seperti ini:

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

Dalam .NET Anda akan menemukan bahwa kelas LinkedList <T> bahkan tidak memiliki pengindeks, jadi Anda tidak akan bisa melakukan perulangan for pada daftar tertaut; tetapi jika Anda bisa, pengindeks harus ditulis seperti ini:

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

Seperti yang Anda lihat, memanggil ini beberapa kali dalam satu putaran akan menjadi jauh lebih lambat daripada menggunakan enumerator yang dapat mengingat di mana posisinya dalam daftar.

Martin Brown
sumber
12

Tes mudah untuk semi-validasi. Saya melakukan tes kecil, hanya untuk melihat. Ini kodenya:

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

}

Dan inilah bagian foreach:

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

Ketika saya mengganti for dengan foreach - foreach 20 milidetik lebih cepat - secara konsisten . Untuk adalah 135-139ms sedangkan foreach adalah 113-119ms. Saya bertukar bolak-balik beberapa kali, memastikan itu bukan proses yang baru saja dimulai.

Namun, ketika saya menghapus foo dan pernyataan if, untuk lebih cepat 30 md (untuk masing-masing 88 md dan untuk 59 md). Keduanya adalah cangkang kosong. Saya berasumsi bahwa foreach benar-benar melewati variabel dimana for hanya menaikkan variabel. Jika saya menambahkan

int foo = intList[i];

Kemudian untuk menjadi lambat sekitar 30ms. Saya berasumsi ini ada hubungannya dengan itu membuat foo dan mengambil variabel dalam array dan menugaskannya ke foo. Jika Anda hanya mengakses intList [i] maka Anda tidak memiliki penalti itu.

Sejujurnya .. Saya berharap foreach menjadi sedikit lebih lambat dalam semua keadaan, tetapi tidak cukup penting di sebagian besar aplikasi.

edit: berikut adalah kode baru menggunakan saran Jons (134217728 adalah int terbesar yang dapat Anda miliki sebelum pengecualian System.OutOfMemory dilempar):

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

Dan inilah hasilnya:

Menghasilkan data. Menghitung loop for: 2458ms Menghitung foreach loop: 2005ms

Menukar mereka untuk melihat apakah itu berkaitan dengan urutan hal-hal menghasilkan hasil yang sama (hampir).

Kenny Mann
sumber
6
Lebih baik menggunakan Stopwatch daripada DateTime. Sekarang - dan saya tidak akan mempercayai lari secepat itu, jujur ​​saja.
Jon Skeet
8
Perulangan foreach Anda berjalan lebih cepat karena 'untuk' mengevaluasi kondisi setiap iterasi. Dalam kasus contoh Anda, ini membuat satu panggilan metode tambahan (untuk mendapatkan list.count) Singkatnya, Anda melakukan benchmarking pada dua bagian kode yang berbeda, maka hasil Anda aneh. Coba 'int max = intlist.Count; for (int i = 0; i <max; i ++) ... 'dan loop' for 'akan selalu berjalan lebih cepat, seperti yang diharapkan!
AR
1
Setelah kompilasi, for dan foreach mengoptimalkan ke hal yang persis sama saat bekerja dengan primitif. Baru setelah Anda memperkenalkan List <T>, kecepatannya berbeda (sangat).
Anthony Russell
9

Catatan: jawaban ini lebih berlaku untuk Java daripada C #, karena C # tidak memiliki pengindeks LinkedLists, tapi saya pikir poin umum masih berlaku.

Jika yang listAnda kerjakan kebetulan adalah a LinkedList, kinerja kode pengindeks ( pengaksesan gaya larik ) jauh lebih buruk daripada menggunakan IEnumeratordari foreach, untuk daftar besar.

Saat Anda mengakses elemen 10.000 dalam LinkedListmenggunakan sintaks pengindeks :, list[10000]daftar tertaut akan dimulai pada simpul kepala, dan melintasi Next-pointer sepuluh ribu kali, hingga mencapai objek yang benar. Jelas, jika Anda melakukan ini dalam satu lingkaran, Anda akan mendapatkan:

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

Saat Anda memanggil GetEnumerator(secara implisit menggunakan forach-syntax), Anda akan mendapatkan IEnumeratorobjek yang memiliki penunjuk ke node kepala. Setiap kali Anda memanggil MoveNext, penunjuk tersebut dipindahkan ke node berikutnya, seperti:

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.

Seperti yang Anda lihat, dalam kasus LinkedLists, metode pengindeks array menjadi lebih lambat dan lebih lambat, semakin lama Anda melakukan perulangan (harus melalui penunjuk kepala yang sama berulang kali). Padahal yang IEnumerableadil beroperasi dalam waktu yang konstan.

Tentu saja, seperti yang dikatakan Jon ini sangat tergantung pada jenisnya list, jika listbukan a LinkedList, tapi sebuah array, perilakunya sangat berbeda.

Tom Lokhorst
sumber
4
LinkedList di .NET tidak memiliki pengindeks, jadi ini sebenarnya bukan opsi.
Jon Skeet
Oh, itu memecahkan masalah itu, maka :-) Saya baru saja melihat-lihat LinkedList<T>dokumen di MSDN, dan ini memiliki API yang lumayan bagus. Yang terpenting, ia tidak memiliki get(int index)metode, seperti Java. Namun, saya kira intinya masih berlaku untuk struktur data seperti daftar lainnya yang mengekspos pengindeks yang lebih lambat dari yang spesifik IEnumerator.
Tom Lokhorst
2

Seperti yang orang lain sebutkan meskipun kinerja sebenarnya tidak terlalu penting, foreach akan selalu sedikit lebih lambat karena IEnumerable/ IEnumeratorpenggunaan dalam loop. Kompilator menerjemahkan konstruksi menjadi panggilan pada antarmuka itu dan untuk setiap langkah fungsi + properti dipanggil dalam konstruksi foreach.

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

Ini adalah ekspansi ekuivalen dari konstruksi di C #. Anda dapat membayangkan bagaimana dampak kinerja dapat bervariasi berdasarkan implementasi MoveNext dan Current. Sedangkan dalam akses array, Anda tidak memiliki dependensi itu.

Charles Prakash Dasari
sumber
4
Jangan lupa ada perbedaan antara akses array dan akses pengindeks. Jika list ada di List<T>sini, maka masih ada hit (mungkin sebaris) untuk memanggil pengindeks. Ini tidak seperti itu akses array logam kosong.
Jon Skeet
Sangat benar! Ini adalah eksekusi properti lainnya dan kami bergantung pada penerapannya.
Charles Prakash Dasari
1

Setelah membaca cukup banyak argumen bahwa "loop foreach harus lebih disukai untuk keterbacaan", saya dapat mengatakan bahwa reaksi pertama saya adalah "apa"? Keterbacaan, secara umum, bersifat subjektif dan, dalam contoh khusus ini, bahkan lebih. Untuk seseorang dengan latar belakang pemrograman (praktis, setiap bahasa sebelum Java), foreach lebih mudah dibaca daripada foreach loop. Selain itu, orang yang sama mengklaim bahwa foreach loop lebih mudah dibaca, juga merupakan pendukung LINQ dan "fitur" lain yang membuat kode sulit dibaca dan dipelihara, sesuatu yang membuktikan poin di atas.

Tentang pengaruhnya terhadap kinerja, lihat jawaban atas pertanyaan ini .

EDIT: Ada koleksi di C # (seperti HashSet) yang tidak memiliki pengindeks. Dalam koleksi ini, foreach adalah satu-satunya cara untuk mengulang dan itu satu-satunya kasus yang menurut saya harus digunakan untuk itu .

ThunderGr
sumber
0

Ada fakta menarik lainnya yang dapat dengan mudah terlewatkan saat menguji kecepatan kedua loop: Menggunakan mode debug tidak memungkinkan kompiler mengoptimalkan kode menggunakan pengaturan default.

Ini membawa saya ke hasil yang menarik bahwa foreach lebih cepat daripada dalam mode debug. Sedangkan for ist lebih cepat dari foreach dalam mode rilis. Jelas compiler memiliki cara yang lebih baik untuk mengoptimalkan perulangan for daripada foreach loop yang mengkompromikan beberapa pemanggilan metode. Perulangan for adalah cara yang sangat mendasar sehingga mungkin saja ini dioptimalkan oleh CPU itu sendiri.

Sam
sumber
0

Dalam contoh yang Anda berikan, pasti lebih baik menggunakan foreachloop daripada forloop.

foreachKonstruksi standar bisa lebih cepat (1,5 siklus per langkah) daripada yang sederhana for-loop(2 siklus per langkah), kecuali loop telah dibuka (1,0 siklus per langkah).

Jadi untuk kode sehari-hari, kinerja bukanlah alasan untuk menggunakan yang lebih kompleks for, whileatau do-whilekonstruksi.

Lihat tautan ini: 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
sumber
4
Anda dapat membaca kembali artikel proyek kode yang Anda tautkan. Ini adalah artikel yang menarik, tetapi dikatakan kebalikan dari posting Anda. Selain itu, tabel yang Anda buat ulang mengukur kinerja mengakses larik dan Daftar secara langsung, atau melalui antarmuka IList mereka. Tidak ada hubungannya dengan pertanyaan itu. :)
Paul Walls
0

Anda dapat membacanya di Deep .NET - Iterasi bagian 1

itu mencakup hasil (tanpa inisialisasi pertama) dari kode sumber .NET sampai ke pembongkaran.

misalnya - Array Iteration dengan foreach loop: masukkan deskripsi gambar di sini

dan - daftar iterasi dengan foreach loop: masukkan deskripsi gambar di sini

dan hasil akhirnya: masukkan deskripsi gambar di sini

masukkan deskripsi gambar di sini

Atau Yaacov
sumber