Foreach döngüsü ve değişken başlatma


11

Kodun bu iki sürümü arasında bir fark var mı?

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

Yoksa derleyici umursamıyor mu? Farkdan bahsettiğimde performans ve bellek kullanımı açısından kastediyorum. ..Ya da temelde herhangi bir fark var mı yoksa ikisi de derlemeden sonra aynı kod mu oluyor?


6
İkisini derlemeyi ve bayt kodu çıktısına bakmayı denediniz mi?

4
@MichaelT Bayt kodu çıktısını karşılaştırmak için nitelikli olduğumu düşünmüyorum .. Bir fark bulursam bunun tam olarak ne anlama geldiğini anlayabileceğimden emin değilim.
Alternatex

4
Eğer aynıysa, kalifiye olmanıza gerek yoktur.

1
@MichaelT Derleyicinin onu optimize edip edemeyeceği ve hangi koşullar altında bu optimizasyonu yapabileceği konusunda iyi bir tahminde bulunacak kadar nitelikli olmanız gerekir.
Ben Aaronson

@BenAaronson ve muhtemelen bu işlevselliği gıdıklamak için önemsiz bir örnek gerektirir.

Yanıtlar:


22

TL; DR - bunlar IL katmanında eşdeğer örneklerdir.


DotNetFiddle , ortaya çıkan IL'yi görmenize izin verdiği için bunu cevaplamayı güzelleştirir .

Testimi daha hızlı hale getirmek için döngü yapınızın biraz farklı bir varyasyonunu kullandım. Kullandım:

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

Varyasyon 2:

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

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

Her iki durumda da, derlenmiş IL çıktısı aynı hale geldi.

.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

Sorunuzu cevaplamak için: derleyici değişkenin bildirimini optimize eder ve iki varyasyonu eşdeğer hale getirir.

Anladığım kadarıyla, .NET IL derleyicisi tüm değişken bildirimleri işlevin başlangıcına taşır, ancak açıkça belirttiğim iyi bir kaynak bulamadım 2 . Bu belirli örnekte, bu ifadeyle onları yukarı taşıdığını görüyorsunuz:

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

Burada karşılaştırma yaparken biraz takıntılı oluruz ....

Durum A, tüm değişkenler yukarı taşınır mı?

Bunu biraz daha ayrıntılı incelemek için aşağıdaki işlevi test ettim:

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

Buradaki fark , karşılaştırmaya dayanarak bir int iveya bir beyan string jetmemizdir. Derleyici yine tüm yerel değişkenleri fonksiyon 2'nin üstüne taşır :

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

int iBu örnekte bildirilmeyecek olsa da, bunu desteklemek için kodun hala oluşturulduğunu belirtmek ilginç buldum .

Durum B: Peki ya foreachyerine for?

Bundan foreachfarklı davranışları olduğu forve sorulanla aynı şeyi kontrol etmediğime dikkat çekildi. Sonuçta ortaya çıkan IL karşılaştırmak için bu iki kod bölümünü koydum.

int döngü dışında bildirim:

    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 Döngü içindeki bildirim:

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

İlme ile elde edilen IL, foreachilmeğin kullanarak üretilen IL'den gerçekten farklıydı for. Özellikle, init bloğu ve döngü bölümü değişti.

.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

foreachYaklaşım daha yerel değişkenler oluşturulur ve bazı ek dallanma gereklidir. Esasen, ilk kez sayımın ilk yinelemesini elde etmek için döngünün sonuna atlar ve daha sonra döngü kodunu yürütmek için döngünün neredeyse üstüne geri atlar. Daha sonra beklediğiniz gibi dönmeye devam eder.

Ancak forve foreachyapılarının kullanılmasının neden olduğu dallanma farklılıklarının ötesinde, bildirimin nereye yerleştirildiğine bağlı olarak IL'de fark yoktuint i . Yani hala iki yaklaşımın eşdeğeriyiz.

Durum C: Farklı derleyici sürümleri ne olacak?

1'de bırakılan bir yorumda, foreach ile değişken erişim ve kapatma kullanma hakkında bir uyarı ile ilgili bir SO sorusuna bir bağlantı vardı . Bu soruda gerçekten dikkatimi çeken kısım, .NET 4.5 derleyicisinin derleyicinin önceki sürümlerine göre çalışma biçiminde farklılıklar olabileceğiydi.

İşte DotNetFiddler sitesi beni hayal kırıklığına uğrattı - mevcut olan tek şey .NET 4.5 ve Roslyn derleyicisinin bir versiyonuydu. Bu yüzden Visual Studio'nun yerel bir örneğini getirdim ve kodu test etmeye başladım. Aynı şeyleri karşılaştırmak emin olmak için .NET 4.5 yerel olarak oluşturulan kodu DotNetFiddler kodu ile karşılaştırdık.

Belirttiğim tek fark, yerel init bloğu ve değişken bildirimi ile oldu. Yerel derleyici değişkenlerin isimlendirilmesinde biraz daha spesifikti.

  .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)

Ama bu küçük farkla, şimdiye kadar çok iyiydi. DotNetFiddler derleyicisi ile yerel VS örneğimin ürettiği şey arasında eşdeğer IL çıkışı vardı.

Daha sonra .NET 4, .NET 3.5 ve iyi ölçmek için .NET 3.5 Release modu hedefleyen proje yeniden.

Ve bu ek vakaların üçünde de üretilen IL eşdeğerdi. Hedeflenen .NET sürümünün, bu örneklerde oluşturulan IL üzerinde hiçbir etkisi yoktur.


Bu macerayı özetlemek gerekirse: Derleyicinin ilkel türü nerede bildirdiğinizi umursamadığını ve her iki bildirim yöntemiyle bellek veya performans üzerinde hiçbir etkisi olmadığını güvenle söyleyebiliriz. Ve bu, a forveya foreachloop kullanmasına bakılmaksızın geçerlidir .

foreachDöngünün içine bir kapanış içeren başka bir vaka çalıştırmayı düşündüm . Fakat ilkel tip değişkeninin nerede bildirildiğini sormuştunuz, bu yüzden sormak istediğiniz şeyin çok ötesine geçtiğimi düşündüm. Daha önce bahsettiğim SO sorusunun, her bir yineleme değişkeni üzerindeki kapatma etkileri hakkında iyi bir genel bakış sağlayan harika bir cevabı var.

1 Andy'ye, foreachdöngülerdeki kapanışları ele alan SO sorusuna orijinal bağlantıyı sağladığı için teşekkür ederiz .

2 ECMA-335 spesifikasyonunun buna I.12.3.2.2 'Yerel değişkenler ve argümanlar' bölümü ile hitap ettiğini belirtmek gerekir . Ortaya çıkan IL'yi görmek ve daha sonra neler olup bittiğine dair net olması için bölümü okumak zorunda kaldım. Bunu sohbete işaret eden mandal ucube sayesinde.


1
Foreach ve foreach aynı davranmaz ve soru, döngüde bir kapatma olduğunda önemli hale gelen farklı olan kodu içerir. stackoverflow.com/questions/14907987/…
Andy

1
@Andy - bağlantı için teşekkürler! Devam ettim ve üretilen çıktıyı bir foreachdöngü kullanarak kontrol ettim ve hedeflenen .NET sürümünü de kontrol ettim.

0

Hangi derleyiciyi kullandığınıza bağlı olarak (C # 'ın birden fazla olup olmadığını bilmiyorum), kodunuz bir programa dönüştürülmeden önce optimize edilecektir. İyi bir derleyici, her seferinde farklı bir değerle aynı değişkeni yeniden başlattığınızı ve bunun için bellek alanını verimli bir şekilde yönettiğinizi görecektir.

Her seferinde aynı değişkeni bir sabite başlatırsanız, derleyici de döngüden önce onu başlatır ve ona başvurur .

Her şey derleyicinizin ne kadar iyi yazıldığına bağlıdır, ancak kodlama standartları söz konusu olduğunda, değişkenler her zaman mümkün olan en az kapsama sahip olmalıdır . Bu yüzden döngü içinde ilan etmek bana her zaman öğretildi.


3
Son paragrafınızın doğru olup olmadığı iki şeye bağlıdır: değişkenin kapsamını kendi programınızın benzersiz bağlamında en aza indirmenin önemi ve derleyicinin birden çok ödevi gerçekten optimize edip etmemesine ilişkin bilgi.
Robert Harvey

Ve daha sonra bayt kodunu makine diline çeviren çalışma zamanı var, burada aynı optimizasyonların birçoğu (burada derleyici optimizasyonları olarak ele alınmaktadır).
Erik Eidt

-2

ilk önce sadece döngü içinde bildirir ve başlatırsınız, böylece döngü her döngüye girdiğinde "i" yeniden döngüye sokulur. İkinci olarak, sadece döngü dışında bildiriyorsunuz.


1
Bu, 2 yıl önce yayınlanan en iyi yanıtta yapılan ve açıklanan puanlar üzerinde önemli bir şey sunmuyor gibi görünüyor
gnat

2
Bir cevap verdiğiniz için teşekkür ederiz, ancak kabul edilen, en yüksek puanlı cevabı zaten kapsamaz ( yeni) herhangi bir yeni yön vermez.
CharonX
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.