Loop depan dan inisialisasi variabel

11

Apakah ada perbedaan antara kedua versi kode ini?

foreach (var thing in things)
{
    int i = thing.number;
    // code using 'i'
    // pay no attention to the uselessness of 'i'
}

int i;
foreach (var thing in things)
{
    i = thing.number;
    // code using 'i'
}

Atau apakah kompiler tidak peduli? Ketika saya berbicara tentang perbedaan, saya maksudkan dalam hal kinerja dan penggunaan memori. ..Atau pada dasarnya hanya ada perbedaan atau apakah keduanya akhirnya menjadi kode yang sama setelah kompilasi?

Alternatex
sumber
6
Sudahkah Anda mencoba mengkompilasi keduanya dan melihat keluaran bytecode?
4
@MichaelT Saya tidak merasa seperti saya memenuhi syarat untuk membandingkan output bytecode .. Jika saya menemukan perbedaan saya tidak yakin saya bisa mengerti apa artinya sebenarnya.
Alternatex
4
Jika sama, Anda tidak perlu memiliki kualifikasi.
1
@MichaelT Meskipun Anda harus cukup berkualitas untuk membuat tebakan yang baik tentang apakah kompiler dapat mengoptimalkannya, dan jika demikian dalam kondisi apa ia dapat melakukan optimasi itu.
Ben Aaronson
@ BenAaronson dan itu mungkin membutuhkan contoh non-sepele untuk menggelitik fungsi itu.

Jawaban:

22

TL; DR - mereka adalah contoh yang setara di lapisan IL.


DotNetFiddle membuat ini cukup untuk menjawab karena memungkinkan Anda untuk melihat IL yang dihasilkan.

Saya menggunakan variasi yang sedikit berbeda dari konstruksi loop Anda untuk membuat pengujian saya lebih cepat. Saya menggunakan:

Variasi 1:

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Hello World");
        int x;
        int i;

        for(x=0; x<=2; x++)
        {
            i = x;
            Console.WriteLine(i);
        }
    }
}

Variasi 2:

        Console.WriteLine("Hello World");
        int x;

        for(x=0; x<=2; x++)
        {
            int i = x;
            Console.WriteLine(i);
        }

Dalam kedua kasus, output IL yang dikompilasi menghasilkan hal yang sama.

.class public auto ansi beforefieldinit Program
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main() cil managed
  {
    // 
    .maxstack  2
    .locals init (int32 V_0,
             int32 V_1,
             bool V_2)
    IL_0000:  nop
    IL_0001:  ldstr      "Hello World"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ldc.i4.0
    IL_000d:  stloc.0
    IL_000e:  br.s       IL_001f

    IL_0010:  nop
    IL_0011:  ldloc.0
    IL_0012:  stloc.1
    IL_0013:  ldloc.1
    IL_0014:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0019:  nop
    IL_001a:  nop
    IL_001b:  ldloc.0
    IL_001c:  ldc.i4.1
    IL_001d:  add
    IL_001e:  stloc.0
    IL_001f:  ldloc.0
    IL_0020:  ldc.i4.2
    IL_0021:  cgt
    IL_0023:  ldc.i4.0
    IL_0024:  ceq
    IL_0026:  stloc.2
    IL_0027:  ldloc.2
    IL_0028:  brtrue.s   IL_0010

    IL_002a:  ret
  } // end of method Program::Main

Jadi untuk menjawab pertanyaan Anda: kompiler mengoptimalkan deklarasi variabel, dan membuat dua variasi yang setara.

Untuk pemahaman saya, .NET IL compiler memindahkan semua deklarasi variabel ke awal fungsi tetapi saya tidak dapat menemukan sumber yang baik yang dengan jelas menyatakan bahwa 2 . Dalam contoh khusus ini, Anda melihat bahwa itu menggerakkan mereka dengan pernyataan ini:

    .locals init (int32 V_0,
             int32 V_1,
             bool V_2)

Di mana kita menjadi agak terlalu obsesif dalam membuat perbandingan ....

Kasus A, apakah semua variabel dipindahkan ke atas?

Untuk menggali sedikit lebih jauh, saya menguji fungsi berikut:

public static void Main()
{
    Console.WriteLine("Hello World");
    int x=5;

    if (x % 2==0) 
    { 
        int i = x; 
        Console.WriteLine(i); 
    }
    else 
    { 
        string j = x.ToString(); 
        Console.WriteLine(j); 
    } 
}

Perbedaannya di sini adalah bahwa kami menyatakan salah int iatau string jberdasarkan perbandingan. Sekali lagi, kompilator memindahkan semua variabel lokal ke atas fungsi 2 dengan:

.locals init (int32 V_0,
         int32 V_1,
         string V_2,
         bool V_3)

Saya merasa menarik untuk dicatat bahwa meskipun int itidak akan dideklarasikan dalam contoh ini, kode untuk mendukungnya masih dihasilkan.

Kasus B: Bagaimana kalau foreachbukan for?

Itu menunjukkan bahwa foreachmemiliki perilaku yang berbeda dari fordan bahwa saya tidak memeriksa hal yang sama yang telah ditanyakan. Jadi saya memasukkan dua bagian kode ini untuk membandingkan IL yang dihasilkan.

int deklarasi di luar loop:

    Console.WriteLine("Hello World");
    List<int> things = new List<int>(){1, 2, 3, 4, 5};
    int i;

    foreach(var thing in things)
    {
        i = thing;
        Console.WriteLine(i);
    }

int deklarasi di dalam loop:

    Console.WriteLine("Hello World");
    List<int> things = new List<int>(){1, 2, 3, 4, 5};

    foreach(var thing in things)
    {
        int i = thing;
        Console.WriteLine(i);
    }

IL yang dihasilkan dengan foreachloop itu memang berbeda dari IL yang dihasilkan menggunakan forloop. Secara khusus, blok init dan bagian loop berubah.

.locals init (class [mscorlib]System.Collections.Generic.List`1<int32> V_0,
         int32 V_1,
         int32 V_2,
         class [mscorlib]System.Collections.Generic.List`1<int32> V_3,
         valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> V_4,
         bool V_5)
...
.try
{
  IL_0045:  br.s       IL_005a

  IL_0047:  ldloca.s   V_4
  IL_0049:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
  IL_004e:  stloc.1
  IL_004f:  nop
  IL_0050:  ldloc.1
  IL_0051:  stloc.2
  IL_0052:  ldloc.2
  IL_0053:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0058:  nop
  IL_0059:  nop
  IL_005a:  ldloca.s   V_4
  IL_005c:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
  IL_0061:  stloc.s    V_5
  IL_0063:  ldloc.s    V_5
  IL_0065:  brtrue.s   IL_0047

  IL_0067:  leave.s    IL_0078

}  // end .try
finally
{
  IL_0069:  ldloca.s   V_4
  IL_006b:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
  IL_0071:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
  IL_0076:  nop
  IL_0077:  endfinally
}  // end handler

The foreachPendekatan yang dihasilkan lebih variabel lokal dan diperlukan beberapa percabangan tambahan. Pada dasarnya, pada saat pertama di dalamnya melompat ke ujung loop untuk mendapatkan iterasi pertama enumerasi dan kemudian melompat kembali ke hampir bagian atas loop untuk menjalankan kode loop. Kemudian terus berlanjut seperti yang Anda harapkan.

Tetapi di luar perbedaan percabangan yang disebabkan oleh penggunaan fordan foreachkonstruksi, tidak ada perbedaan dalam IL berdasarkan di mana int ideklarasi ditempatkan. Jadi kita masih pada dua pendekatan yang setara.

Kasus C: Bagaimana dengan versi kompiler yang berbeda?

Dalam komentar yang tersisa 1 , ada tautan ke pertanyaan SO tentang peringatan tentang akses variabel dengan foreach dan menggunakan penutupan . Bagian yang benar-benar menarik perhatian saya dalam pertanyaan itu adalah bahwa mungkin ada perbedaan dalam cara kerja kompiler .NET 4.5 dibandingkan versi sebelumnya dari kompiler.

Dan di situlah situs DotNetFiddler mengecewakan saya - yang mereka miliki hanyalah .NET 4.5 dan versi dari kompiler Roslyn. Jadi saya membuka contoh lokal Visual Studio dan mulai menguji kode. Untuk memastikan saya membandingkan hal yang sama, saya membandingkan kode yang dibuat secara lokal di .NET 4.5 dengan kode DotNetFiddler.

Satu-satunya perbedaan yang saya perhatikan adalah dengan blok init lokal dan deklarasi variabel. Kompiler lokal sedikit lebih spesifik dalam penamaan variabel.

  .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> things,
           [1] int32 thing,
           [2] int32 i,
           [3] class [mscorlib]System.Collections.Generic.List`1<int32> '<>g__initLocal0',
           [4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0000,
           [5] bool CS$4$0001)

Tetapi dengan perbedaan kecil itu, sejauh ini, sangat bagus. Saya memiliki output IL yang setara antara kompiler DotNetFiddler dan apa yang diproduksi oleh instance VS lokal saya.

Jadi saya kemudian membangun kembali penargetan proyek .NET 4, .NET 3.5, dan untuk ukuran yang baik. NET 3.5 Rilis mode.

Dan dalam ketiga kasus tambahan tersebut, IL yang dihasilkan setara. Versi .NET yang ditargetkan tidak berpengaruh pada IL yang dihasilkan dalam sampel ini.


Untuk merangkum petualangan ini: Saya pikir kita dapat dengan yakin mengatakan bahwa kompiler tidak peduli di mana Anda mendeklarasikan tipe primitif dan bahwa tidak ada efek pada memori atau kinerja dengan metode deklarasi. Dan itu berlaku terlepas dari menggunakan a foratau foreachloop.

Saya mempertimbangkan menjalankan kasus lain yang memasukkan penutupan di dalam foreachloop. Tetapi Anda telah bertanya tentang efek di mana variabel tipe primitif dideklarasikan, jadi saya pikir saya menggali terlalu jauh melampaui apa yang Anda minati. Pertanyaan SO yang saya sebutkan sebelumnya memiliki jawaban yang bagus yang memberikan tinjauan yang baik tentang efek penutupan pada variabel iteach masing-masing.

1 Terima kasih kepada Andy karena telah memberikan tautan asli ke pertanyaan SO yang membahas penutupan dalam foreachloop.

2 Perlu dicatat bahwa spesifikasi ECMA-335 membahas hal ini dengan bagian I.12.3.2.2 'Variabel dan argumen lokal'. Saya harus melihat IL yang dihasilkan dan kemudian membaca bagian untuk menjadi jelas tentang apa yang sedang terjadi. Terima kasih kepada ratchet freak karena menunjukkan hal itu dalam obrolan.

Komunitas
sumber
1
For dan foreach tidak berlaku sama, dan pertanyaan mencakup kode yang berbeda yang menjadi penting ketika ada penutupan dalam loop. stackoverflow.com/questions/14907987/…
Andy
1
@Andy - terima kasih atas tautannya! Saya pergi ke depan dan memeriksa output yang dihasilkan menggunakan foreachloop dan juga memeriksa versi .NET yang ditargetkan.
0

Bergantung pada kompiler apa yang Anda gunakan (Saya bahkan tidak tahu apakah C # memiliki lebih dari satu), kode Anda akan dioptimalkan sebelum diubah menjadi program. Kompiler yang baik akan melihat bahwa Anda menginisialisasi ulang variabel yang sama setiap kali dengan nilai yang berbeda dan mengelola ruang memori untuknya secara efisien.

Jika Anda menginisialisasi variabel yang sama ke konstanta setiap kali, kompiler juga akan menginisialisasinya sebelum loop dan referensi itu.

Itu semua tergantung seberapa baik kompiler Anda ditulis, tetapi sejauh standar pengkodean yang bersangkutan variabel harus selalu memiliki ruang lingkup sesedikit mungkin . Jadi menyatakan di dalam lingkaran adalah apa yang saya selalu diajarkan.

leylandski
sumber
3
Apakah paragraf terakhir Anda benar atau tidak tergantung pada dua hal: pentingnya meminimalkan ruang lingkup variabel dalam konteks unik program Anda sendiri, dan pengetahuan orang dalam tentang kompiler, apakah itu benar-benar mengoptimalkan penugasan ganda atau tidak.
Robert Harvey
Dan kemudian ada runtime, yang selanjutnya menerjemahkan kode byte ke dalam bahasa mesin, di mana banyak dari optimasi yang sama ini (sedang dibahas di sini sebagai optimasi kompiler) juga dilakukan.
Erik Eidt
-2

pada awalnya Anda hanya mendeklarasikan dan menginisialisasi di dalam loop sehingga setiap kali loop itu akan diinisialisasi ulang "i" di dalam loop. Di detik Anda hanya mendeklarasikan di luar loop.

pengguna304046
sumber
1
ini tampaknya tidak menawarkan sesuatu yang substansial atas poin yang dibuat dan dijelaskan dalam jawaban teratas yang diposting lebih dari 2 tahun yang lalu
agas
2
Terima kasih telah memberikan jawaban, tetapi tidak memberikan aspek baru apa pun yang diterima, jawaban berperingkat teratas belum mencakup (secara terperinci).
CharonX