Vòng lặp foreach và khởi tạo biến


11

Có sự khác biệt giữa hai phiên bản mã này không?

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

Hay trình biên dịch không quan tâm? Khi tôi nói về sự khác biệt tôi có nghĩa là về hiệu suất và sử dụng bộ nhớ. .. Về cơ bản chỉ là bất kỳ sự khác biệt hoặc hai cuối cùng là cùng một mã sau khi biên dịch?


6
Bạn đã thử biên dịch cả hai và nhìn vào đầu ra mã byte chưa?

4
@MichaelT Tôi không cảm thấy mình đủ điều kiện để so sánh đầu ra của mã byte .. Nếu tôi tìm thấy một sự khác biệt Tôi không chắc mình có thể hiểu chính xác ý nghĩa của nó.
Alternatex

4
Nếu nó giống nhau, bạn không cần phải đủ điều kiện.

1
@MichaelT Mặc dù bạn cần phải có đủ điều kiện để đoán đúng về việc trình biên dịch có thể tối ưu hóa nó hay không, và nếu vậy trong điều kiện nào thì nó có thể thực hiện tối ưu hóa đó.
Ben Aaronson

@BenAaronson và điều đó có thể đòi hỏi một ví dụ không tầm thường để đánh dấu chức năng đó.

Câu trả lời:


22

TL; DR - chúng là những ví dụ tương đương ở lớp IL.


DotNetFiddle làm cho câu trả lời này khá hay vì nó cho phép bạn xem IL kết quả.

Tôi đã sử dụng một biến thể hơi khác nhau của cấu trúc vòng lặp của bạn để làm cho thử nghiệm của tôi nhanh hơn. Tôi đã sử dụng:

Biến thể 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);
        }
    }
}

Biến thể 2:

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

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

Trong cả hai trường hợp, đầu ra IL được biên dịch đều giống nhau.

.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

Vì vậy, để trả lời câu hỏi của bạn: trình biên dịch tối ưu hóa khai báo biến và hiển thị hai biến thể tương đương.

Theo hiểu biết của tôi, trình biên dịch .NET IL di chuyển tất cả các khai báo biến sang đầu hàm nhưng tôi không thể tìm thấy một nguồn tốt có ghi rõ ràng rằng 2 . Trong ví dụ cụ thể này, bạn thấy rằng nó đã di chuyển chúng lên với câu lệnh này:

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

Trong đó chúng ta có một chút quá ám ảnh trong việc so sánh ....

Trường hợp A, tất cả các biến được di chuyển lên?

Để tìm hiểu sâu hơn một chút, tôi đã thử nghiệm chức năng sau:

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

Sự khác biệt ở đây là chúng tôi tuyên bố một int ihoặc string jdựa trên so sánh. Một lần nữa, trình biên dịch di chuyển tất cả các biến cục bộ lên đầu hàm 2 với:

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

Tôi thấy thật thú vị khi lưu ý rằng mặc dù int isẽ không được khai báo trong ví dụ này, mã để hỗ trợ nó vẫn được tạo.

Trường hợp B: Thế còn foreachthay vì for?

Nó đã chỉ ra rằng foreachcó hành vi khác với forvà tôi đã không kiểm tra điều tương tự đã được hỏi về. Vì vậy, tôi đưa vào hai phần mã này để so sánh IL kết quả.

int khai báo bên ngoài vòng lặp:

    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 khai báo bên trong vòng lặp:

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

Kết quả IL với foreachvòng lặp thực sự khác với IL được tạo bằng forvòng lặp. Cụ thể, khối init và phần vòng lặp đã thay đổi.

.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

Cách foreachtiếp cận tạo ra nhiều biến cục bộ hơn và yêu cầu một số nhánh bổ sung. Về cơ bản, trong lần đầu tiên, nó nhảy đến cuối vòng lặp để có lần lặp đầu tiên của phép liệt kê và sau đó nhảy trở lại gần như đỉnh của vòng lặp để thực thi mã vòng lặp. Sau đó nó tiếp tục lặp lại như bạn mong đợi.

Nhưng ngoài sự khác biệt phân nhánh gây ra bởi việc sử dụng forforeachcấu trúc, không có sự khác biệt nào trong IL dựa trên vị trí int ikhai báo được đặt. Vì vậy, chúng tôi vẫn ở hai cách tiếp cận tương đương nhau.

Trường hợp C: Còn các phiên bản trình biên dịch khác nhau thì sao?

Trong một bình luận còn lại 1 , có một liên kết đến một câu hỏi SO liên quan đến một cảnh báo về quyền truy cập biến đổi với foreach và sử dụng bao đóng . Phần thực sự khiến tôi chú ý trong câu hỏi đó là có thể có sự khác biệt về cách trình biên dịch .NET 4.5 hoạt động so với các phiên bản trước đó của trình biên dịch.

Và đó là nơi mà trang web DotNetFiddler làm tôi thất vọng - tất cả những gì họ có là .NET 4.5 và một phiên bản của trình biên dịch Roslyn. Vì vậy, tôi đã đưa ra một phiên bản địa phương của Visual Studio và bắt đầu thử nghiệm mã. Để chắc chắn rằng tôi đang so sánh những điều tương tự, tôi đã so sánh mã được xây dựng cục bộ tại .NET 4.5 với mã DotNetFiddler.

Sự khác biệt duy nhất mà tôi lưu ý là với khối init cục bộ và khai báo biến. Trình biên dịch cục bộ cụ thể hơn một chút trong việc đặt tên các biến.

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

Nhưng với sự khác biệt nhỏ đó, nó đã rất tốt. Tôi đã có đầu ra IL tương đương giữa trình biên dịch DotNetFiddler và phiên bản VS cục bộ của tôi đang tạo ra.

Vì vậy, sau đó tôi đã xây dựng lại dự án nhắm mục tiêu .NET 4, .NET 3.5 và để có chế độ phát hành .NET 3.5 tốt.

Và trong cả ba trường hợp bổ sung đó, IL được tạo ra đều tương đương. Phiên bản .NET được nhắm mục tiêu không có tác dụng đối với IL được tạo trong các mẫu này.


Để tóm tắt cuộc phiêu lưu này: Tôi nghĩ rằng chúng ta có thể tự tin nói rằng trình biên dịch không quan tâm đến việc bạn khai báo kiểu nguyên thủy ở đâu và rằng không có ảnh hưởng đến bộ nhớ hoặc hiệu suất với phương thức khai báo. Và điều đó đúng cho dù sử dụng vòng lặp forhay foreach.

Tôi đã cân nhắc việc chạy một trường hợp khác có kết hợp đóng bên trong foreachvòng lặp. Nhưng bạn đã hỏi về tác động của nơi một biến loại nguyên thủy được khai báo, vì vậy tôi đoán rằng tôi đang đi quá xa so với những gì bạn quan tâm khi hỏi về. Câu hỏi SO tôi đã đề cập trước đó có một câu trả lời tuyệt vời cung cấp một cái nhìn tổng quan tốt về các hiệu ứng đóng đối với các biến lặp foreach.

1 Cảm ơn Andy vì đã cung cấp liên kết ban đầu đến các câu hỏi SO giải quyết các lần đóng trong foreachvòng lặp.

2 Điều đáng chú ý là thông số kỹ thuật ECMA-335 giải quyết vấn đề này với phần I.12.3.2.2 'Các biến và đối số cục bộ'. Tôi đã phải xem IL kết quả và sau đó đọc phần cho nó rõ ràng về những gì đang xảy ra. Cảm ơn ratchet freak đã chỉ ra rằng trong trò chuyện.


1
For và foreach không hành xử giống nhau và câu hỏi bao gồm mã khác nhau, điều này trở nên quan trọng khi có một vòng lặp trong vòng lặp. stackoverflow.com/questions/14907987/ Mạnh
Andy

1
@Andy - cảm ơn vì đường link! Tôi đã tiếp tục và kiểm tra đầu ra được tạo bằng một foreachvòng lặp và cũng kiểm tra phiên bản .NET được nhắm mục tiêu.

0

Tùy thuộc vào trình biên dịch bạn sử dụng (tôi thậm chí không biết nếu C # có nhiều hơn một), mã của bạn sẽ được tối ưu hóa trước khi được chuyển thành chương trình. Một trình biên dịch tốt sẽ thấy rằng bạn đang khởi tạo lại cùng một biến mỗi lần với một giá trị khác nhau và quản lý không gian bộ nhớ cho nó một cách hiệu quả.

Nếu bạn đang khởi tạo cùng một biến thành hằng số mỗi lần, trình biên dịch cũng sẽ khởi tạo nó trước vòng lặp và tham chiếu nó.

Tất cả phụ thuộc vào trình biên dịch của bạn được viết tốt như thế nào, nhưng theo tiêu chuẩn mã hóa là các biến liên quan nên luôn có phạm vi ít nhất có thể . Vì vậy, tuyên bố bên trong vòng lặp là điều tôi luôn được dạy.


3
Đoạn cuối của bạn có đúng hay không phụ thuộc vào hai điều: tầm quan trọng của việc giảm thiểu phạm vi của biến trong bối cảnh duy nhất của chương trình của bạn và kiến ​​thức bên trong trình biên dịch về việc nó có thực sự tối ưu hóa nhiều bài tập hay không.
Robert Harvey

Và sau đó là thời gian chạy, dịch mã byte tiếp theo sang ngôn ngữ máy, trong đó nhiều tối ưu hóa tương tự (được thảo luận ở đây là tối ưu hóa trình biên dịch) cũng được thực hiện.
Erik Eidt

-2

Đầu tiên, bạn chỉ cần khai báo và khởi tạo vòng lặp bên trong, vì vậy mỗi vòng lặp thời gian sẽ lặp lại "i" bên trong vòng lặp. Trong thứ hai, bạn chỉ khai báo bên ngoài vòng lặp.


1
điều này dường như không cung cấp bất cứ điều gì đáng kể qua các điểm được thực hiện và giải thích trong câu trả lời hàng đầu đã được đăng hơn 2 năm trước
gnat

2
Cảm ơn bạn đã đưa ra câu trả lời, nhưng nó không đưa ra bất kỳ khía cạnh mới nào mà câu trả lời được đánh giá cao, được chấp nhận hàng đầu chưa bao gồm (chi tiết).
CharonX
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.