Foreach loop และการกำหนดค่าเริ่มต้นตัวแปร


11

มีความแตกต่างระหว่างโค้ดสองเวอร์ชันนี้หรือไม่?

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

คอมไพเลอร์ไม่สนใจหรือ? เมื่อฉันพูดถึงความแตกต่างฉันหมายถึงในแง่ของประสิทธิภาพและการใช้หน่วยความจำ .. หรือโดยทั่วไปเพียงแค่ความแตกต่างใด ๆ หรือทำสองท้ายเป็นรหัสเดียวกันหลังจากรวบรวม?


6
คุณลองคอมไพล์ทั้งสองแล้วดูผลลัพธ์ของ bytecode หรือไม่?

4
@MichaelT ฉันไม่รู้สึกว่าฉันมีคุณสมบัติพอที่จะเปรียบเทียบผลลัพธ์ของ bytecode .. หากฉันพบความแตกต่างฉันไม่แน่ใจว่าฉันจะสามารถเข้าใจความหมายของมันได้อย่างแม่นยำ
Alternatex

4
หากเป็นเช่นเดียวกันคุณไม่จำเป็นต้องผ่านการรับรอง

1
@MichaelT แม้ว่าคุณจะต้องมีคุณสมบัติเพียงพอที่จะคาดเดาได้ดีว่าคอมไพเลอร์สามารถปรับให้เหมาะสมหรือไม่และหากเป็นเช่นนั้นภายใต้เงื่อนไขใดก็สามารถทำการเพิ่มประสิทธิภาพได้
Ben Aaronson

@BenAaronson และที่อาจต้องใช้ตัวอย่างที่ไม่สำคัญเพื่อจี้ฟังก์ชั่นที่

คำตอบ:


22

TL; DR - เป็นตัวอย่างที่เท่าเทียมกันที่เลเยอร์ IL


DotNetFiddleทำให้คำตอบนี้สวยเพราะช่วยให้คุณเห็นผลลัพท์ของ IL

ฉันใช้รูปแบบวนรอบที่แตกต่างกันเล็กน้อยเพื่อให้การทดสอบของฉันเร็วขึ้น ฉันใช้:

รูปแบบที่ 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);
        }
    }
}

รูปแบบที่ 2:

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

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

ในทั้งสองกรณีเอาต์พุต IL ที่รวบรวมจะแสดงผลเหมือนกัน

.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

ดังนั้นเพื่อตอบคำถามของคุณ: คอมไพเลอร์จะปรับการประกาศตัวแปรให้เหมาะสมที่สุดและทำให้ทั้งสองรูปแบบเทียบเท่ากัน

ความเข้าใจของคอมไพเลอร์ .NET IL ย้ายประกาศตัวแปรทั้งหมดไปยังจุดเริ่มต้นของการทำงาน แต่ฉันไม่สามารถหาแหล่งที่ดีที่ระบุไว้ชัดเจนว่า2 ในตัวอย่างนี้คุณเห็นว่ามันทำให้พวกเขาขึ้นกับคำสั่งนี้:

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

ประเด็นที่เราได้รับการครอบงำมากเกินไปในการเปรียบเทียบ ....

กรณี A ตัวแปรทั้งหมดถูกเลื่อนขึ้นหรือไม่?

ในการขุดลงไปอีกเล็กน้อยฉันได้ทดสอบฟังก์ชั่นต่อไปนี้:

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

ความแตกต่างที่นี่ก็คือเราประกาศอย่างใดอย่างหนึ่งint iหรือstring jตามการเปรียบเทียบ อีกครั้งคอมไพเลอร์ย้ายตัวแปรท้องถิ่นทั้งหมดไปยังด้านบนของฟังก์ชัน2ด้วย:

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

ฉันพบว่าน่าสนใจที่จะทราบว่าแม้ว่าint iจะไม่ได้รับการประกาศในตัวอย่างนี้ แต่รหัสที่ให้การสนับสนุนนั้นยังคงถูกสร้างขึ้น

กรณี B: แล้วforeachแทนที่จะเป็นforอะไรล่ะ

มันชี้ให้เห็นว่าforeachมีพฤติกรรมที่แตกต่างกว่าforและฉันไม่ได้ตรวจสอบสิ่งเดียวกับที่ถูกถามเกี่ยวกับ ดังนั้นฉันจึงใส่โค้ดสองส่วนนี้เพื่อเปรียบเทียบ IL ที่ได้

int ประกาศนอกวง:

    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 ประกาศภายในของวง:

    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 กับforeachลูปนั้นแตกต่างจาก IL ที่สร้างโดยใช้forลูป โดยเฉพาะบล็อก init และส่วนลูปจะเปลี่ยนไป

.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

foreachวิธีการสร้างตัวแปรท้องถิ่นมากขึ้นและจำเป็นบางแขนงเพิ่มเติม โดยพื้นฐานแล้วในครั้งแรกที่มันกระโดดไปยังจุดสิ้นสุดของลูปเพื่อรับการวนซ้ำครั้งแรกของการแจงนับแล้วกระโดดกลับไปที่เกือบด้านบนของลูปเพื่อรันโค้ดลูป จากนั้นมันจะวนซ้ำตามที่คุณคาดไว้

แต่นอกเหนือจากความแตกต่างของการแตกกิ่งก้านที่เกิดจากการใช้forและการforeachสร้างไม่มีความแตกต่างใน IL ตามที่int iวางประกาศไว้ ดังนั้นเรายังอยู่ที่สองวิธีที่เท่าเทียมกัน

กรณี C: แล้วคอมไพเลอร์เวอร์ชั่นต่าง ๆ ล่ะ?

ในความคิดเห็นที่เหลือ1มีการเชื่อมโยงไปอีกด้วยคำถาม SO เกี่ยวกับคำเตือนเกี่ยวกับการเข้าถึงตัวแปรกับ foreach และการใช้ปิด ส่วนที่จับตาของฉันจริงๆในคำถามนั้นก็คืออาจมีความแตกต่างในวิธีการที่คอมไพเลอร์. NET 4.5 ทำงานกับคอมไพเลอร์รุ่นก่อนหน้า

และนั่นคือสิ่งที่เว็บไซต์ DotNetFiddler ทำให้ฉันผิดหวัง - สิ่งที่พวกเขามีอยู่คือ. NET 4.5 และคอมไพเลอร์ Roslyn รุ่นหนึ่ง ดังนั้นฉันจึงนำอินสแตนซ์ในท้องถิ่นของ Visual Studio ขึ้นมาและเริ่มทดสอบโค้ด เพื่อให้แน่ใจว่าฉันกำลังเปรียบเทียบสิ่งเดียวกันฉันเปรียบเทียบโค้ดภายในเครื่องที่. NET 4.5 กับโค้ด DotNetFiddler

ข้อแตกต่างเดียวที่ฉันสังเกตเห็นคือกับการเริ่มต้นบล็อกท้องถิ่นและการประกาศตัวแปร คอมไพเลอร์ในพื้นที่มีความเฉพาะเจาะจงมากกว่านี้เล็กน้อยในการตั้งชื่อตัวแปร

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

แต่ด้วยความแตกต่างเล็กน้อยนั้นมันก็ยังดีอยู่ดี ฉันมีเอาต์พุต IL ที่เท่ากันระหว่างคอมไพเลอร์ DotNetFiddler และอินสแตนซ์ VS ในพื้นที่ของฉันกำลังสร้าง

ดังนั้นฉันจึงสร้างโครงการที่กำหนดเป้าหมาย. NET 4,. NET 3.5 ขึ้นมาใหม่และเพื่อทำการวัดที่ดีในโหมด NET 3.5 Release

และในทั้งสามกรณีเพิ่มเติมนั้น IL ที่ถูกสร้างขึ้นนั้นเทียบเท่ากัน เวอร์ชัน. NET เป้าหมายนั้นไม่มีผลกับ IL ที่สร้างขึ้นในตัวอย่างเหล่านี้


เพื่อสรุปการผจญภัยนี้: ฉันคิดว่าเราสามารถพูดได้อย่างมั่นใจว่าคอมไพเลอร์ไม่สนใจว่าคุณจะประกาศประเภทดั้งเดิมและไม่มีผลต่อหน่วยความจำหรือประสิทธิภาพด้วยวิธีการประกาศอย่างใดอย่างหนึ่ง และนั่นถือเป็นจริงโดยไม่คำนึงถึงการใช้forหรือforeachลูป

ฉันคิดว่ากำลังวิ่งอีกกรณีที่รวมการปิดด้านในของforeachลูป แต่คุณได้ถามเกี่ยวกับผลกระทบของการประกาศตัวแปรประเภทดั้งเดิมดังนั้นฉันจึงคิดว่าฉันกำลังเจาะลึกเกินกว่าที่คุณสนใจถาม คำถาม SO ที่ฉันกล่าวถึงก่อนหน้านี้มีคำตอบที่ดีซึ่งให้ภาพรวมที่ดีเกี่ยวกับผลกระทบการปิดสำหรับตัวแปรการทำซ้ำแต่ละครั้ง

1 ขอบคุณ Andy ที่ให้ลิงก์ดั้งเดิมไปยังการปิดการตอบคำถาม SO ภายในforeachลูป

2 เป็นที่น่าสังเกตว่าข้อมูลจำเพาะของ ECMA-335กล่าวถึงเรื่องนี้ในหัวข้อ I.12.3.2.2 'ตัวแปรและการขัดแย้งในท้องถิ่น' ฉันต้องดู IL ที่เกิดขึ้นแล้วอ่านหัวข้อเพื่อให้ชัดเจนเกี่ยวกับสิ่งที่เกิดขึ้น ขอบคุณวงล้อประหลาดที่ชี้ว่าในการแชท


1
สำหรับและ foreach ไม่ทำงานเหมือนกันและคำถามที่มีรหัสที่แตกต่างซึ่งจะกลายเป็นสิ่งสำคัญเมื่อมีการปิดในวง stackoverflow.com/questions/14907987/…
Andy

1
@Andy - ขอบคุณสำหรับลิงค์! ฉันไปข้างหน้าและตรวจสอบผลลัพธ์ที่สร้างขึ้นโดยใช้foreachลูปและตรวจสอบเวอร์ชัน. NET เป้าหมายด้วย

0

ขึ้นอยู่กับว่าคุณใช้คอมไพเลอร์อะไร (ฉันไม่รู้ด้วยซ้ำว่า C # มีมากกว่าหนึ่งรหัส) โค้ดของคุณจะถูกปรับให้เหมาะสมก่อนที่จะกลายเป็นโปรแกรม คอมไพเลอร์ที่ดีจะเห็นว่าคุณกำลังเริ่มต้นตัวแปรเดียวกันใหม่ทุกครั้งด้วยค่าที่แตกต่างและจัดการพื้นที่หน่วยความจำอย่างมีประสิทธิภาพ

หากคุณกำลังเริ่มต้นตัวแปรเดียวกันให้มีค่าคงที่ในแต่ละครั้งคอมไพเลอร์จะทำการกำหนดค่าเริ่มต้นให้เหมือนกันก่อนลูปและอ้างอิง

ทุกอย่างขึ้นอยู่กับว่าคอมไพเลอร์ของคุณเขียนได้ดีเท่าที่มาตรฐานการเข้ารหัสเป็นตัวแปรที่เกี่ยวข้องควรมีขอบเขตที่เป็นไปได้น้อยที่สุด ดังนั้นการประกาศในวงคือสิ่งที่ฉันได้รับการสอนมาตลอด


3
ไม่ว่าย่อหน้าสุดท้ายของคุณจะเป็นจริงหรือไม่นั้นขึ้นอยู่กับสองสิ่ง: ความสำคัญของการลดขอบเขตของตัวแปรในบริบทเฉพาะของโปรแกรมของคุณเองและความรู้ภายในของคอมไพเลอร์ว่าเหมาะสมหรือไม่
Robert Harvey

จากนั้นก็จะมีรันไทม์ซึ่งแปลรหัสไบต์เป็นภาษาเครื่องอีกด้วยซึ่งการเพิ่มประสิทธิภาพเดียวกันนี้หลายอย่าง
Erik Eidt

-2

ในตอนแรกคุณเพิ่งประกาศและเตรียมใช้งานลูปภายในดังนั้นทุกครั้งที่ลูปลูปจะได้รับการกำหนดค่าเริ่มต้นใหม่เป็น "i" ภายในลูป ในวินาทีคุณจะประกาศนอกวงเท่านั้น


1
สิ่งนี้ดูเหมือนจะไม่เสนอสิ่งที่สำคัญเกินกว่าที่ทำและอธิบายไว้ในคำตอบยอดนิยมที่โพสต์เมื่อ 2 ปีที่แล้ว
gnat

2
ขอบคุณที่ให้คำตอบ แต่ไม่ได้ให้แง่มุมใหม่ ๆ เกี่ยวกับคำตอบที่ได้รับการยอมรับอันดับสูงสุดซึ่งยังไม่ครอบคลุม (โดยละเอียด)
CharonX
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.