2011-12-16 56 views
30

là làm tốt hơn:Tốt hơn là nên khai báo một biến bên trong hay bên ngoài một vòng lặp?

variable1Type foo; 
variable2Type baa; 

foreach(var val in list) 
{ 
    foo = new Foo(...); 
    foo.x = FormatValue(val); 

    baa = new Baa(); 
    baa.main = foo; 
    baa.Do(); 
} 

Hoặc:

foreach(var val in list) 
{ 
    variable1Type foo = new Foo(...); 
    foo.x = FormatValue(val); 

    variable2Type baa = new Baa(); 
    baa.main = foo; 
    baa.Do(); 
} 

Câu hỏi đặt ra là: 1 trường hợp hoặc 2 trường hợp là gì nhanh hơn? Sự khác biệt là không đáng kể? Nó có giống nhau trong các ứng dụng thực tế không? Đây có thể là một tối ưu hóa vi mô, nhưng tôi thực sự muốn biết cái nào tốt hơn.

+1

Không có sự khác biệt về hiệu suất nếu bạn không ghi lại biến trong một lambda. – Jon

+3

Bạn nên đánh giá sự khác biệt ngữ nghĩa và không phải là sự khác biệt hiệu suất, mà trong trường hợp này là không có. Bằng cách khai báo bên trong vòng lặp, bạn tự động biết nó chỉ được sử dụng bên trong vòng lặp. –

+0

Tôi ghét làm điều đó, nhưng -1: câu hỏi của bạn không chỉ ra bất kỳ nỗ lực nghiên cứu nào. Bạn có thể chắc chắn đã thử nghiệm điều này một mình. –

Trả lời

46

Performance-khôn ngoan, chúng ta hãy thử ví dụ cụ thể:

public void Method1() 
{ 
    foreach(int i in Enumerable.Range(0, 10)) 
    { 
    int x = i * i; 
    StringBuilder sb = new StringBuilder(); 
    sb.Append(x); 
    Console.WriteLine(sb); 
    } 
} 
public void Method2() 
{ 
    int x; 
    StringBuilder sb; 
    foreach(int i in Enumerable.Range(0, 10)) 
    { 
    x = i * i; 
    sb = new StringBuilder(); 
    sb.Append(x); 
    Console.WriteLine(sb); 
    } 
} 

tôi cố tình chọn cả một giá trị kiểu và một tham chiếu kiểu trong trường hợp có ảnh hưởng đến mọi thứ. Bây giờ, IL cho họ:

.method public hidebysig instance void Method1() cil managed 
{ 
    .maxstack 2 
    .locals init (
     [0] int32 i, 
     [1] int32 x, 
     [2] class [mscorlib]System.Text.StringBuilder sb, 
     [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> enumerator) 
    L_0000: ldc.i4.0 
    L_0001: ldc.i4.s 10 
    L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32) 
    L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() 
    L_000d: stloc.3 
    L_000e: br.s L_002f 
    L_0010: ldloc.3 
    L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current() 
    L_0016: stloc.0 
    L_0017: ldloc.0 
    L_0018: ldloc.0 
    L_0019: mul 
    L_001a: stloc.1 
    L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor() 
    L_0020: stloc.2 
    L_0021: ldloc.2 
    L_0022: ldloc.1 
    L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32) 
    L_0028: pop 
    L_0029: ldloc.2 
    L_002a: call void [mscorlib]System.Console::WriteLine(object) 
    L_002f: ldloc.3 
    L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() 
    L_0035: brtrue.s L_0010 
    L_0037: leave.s L_0043 
    L_0039: ldloc.3 
    L_003a: brfalse.s L_0042 
    L_003c: ldloc.3 
    L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
    L_0042: endfinally 
    L_0043: ret 
    .try L_000e to L_0039 finally handler L_0039 to L_0043 
} 

.method public hidebysig instance void Method2() cil managed 
{ 
    .maxstack 2 
    .locals init (
     [0] int32 x, 
     [1] class [mscorlib]System.Text.StringBuilder sb, 
     [2] int32 i, 
     [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> enumerator) 
    L_0000: ldc.i4.0 
    L_0001: ldc.i4.s 10 
    L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32) 
    L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() 
    L_000d: stloc.3 
    L_000e: br.s L_002f 
    L_0010: ldloc.3 
    L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current() 
    L_0016: stloc.2 
    L_0017: ldloc.2 
    L_0018: ldloc.2 
    L_0019: mul 
    L_001a: stloc.0 
    L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor() 
    L_0020: stloc.1 
    L_0021: ldloc.1 
    L_0022: ldloc.0 
    L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32) 
    L_0028: pop 
    L_0029: ldloc.1 
    L_002a: call void [mscorlib]System.Console::WriteLine(object) 
    L_002f: ldloc.3 
    L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() 
    L_0035: brtrue.s L_0010 
    L_0037: leave.s L_0043 
    L_0039: ldloc.3 
    L_003a: brfalse.s L_0042 
    L_003c: ldloc.3 
    L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
    L_0042: endfinally 
    L_0043: ret 
    .try L_000e to L_0039 finally handler L_0039 to L_0043 
} 

Như bạn có thể thấy, ngoài các thứ tự trên stack trình biên dịch đã xảy ra để lựa chọn - có thể cũng giống như cũng đã là một thứ tự khác nhau - nó đã hoàn toàn không có hiệu lực. Đổi lại, có thực sự không phải là bất cứ điều gì mà một là cho jitter để sử dụng nhiều mà người khác không phải là cho nó.

Ngoài ra, có một sự khác biệt về loại.

Trong Method1() tôi, xsb được scoped đến foreach, và không thể truy cập hoặc là cố tình hay vô tình bên ngoài của nó.

Trong tôi Method2(), xsb không biết tại thời gian biên dịch được gán đáng tin cậy một giá trị trong foreach (trình biên dịch không biết foreach sẽ thực hiện ít nhất một vòng lặp), vì vậy sử dụng nó bị cấm .

Cho đến nay, không có sự khác biệt thực sự.

tôi thể tuy nhiên gán và sử dụng x và/hoặc sb ngoài foreach. Theo quy định, tôi có thể nói rằng đây có lẽ là phạm vi nghèo nàn hầu hết thời gian, vì vậy tôi ưu tiên Method1, nhưng tôi có thể có một số lý do hợp lý để đề cập đến chúng (thực tế hơn nếu chúng không được bỏ gán), trong trường hợp tôi muốn đi cho Method2.

Tuy nhiên, đó là vấn đề về cách mỗi mã có thể được mở rộng hay không, không phải là sự khác biệt của mã như được viết. Thực sự, không có sự khác biệt.

+2

+1 để tạo trường hợp thử nghiệm và sử dụng nó để cung cấp bằng chứng để hỗ trợ khiếu nại của bạn. Làm tốt lắm. –

+0

Điểm của bạn nói chung là hợp lệ, và tôi cũng sẽ kiểm tra IL nếu tôi không sử dụng Mac. Tuy nhiên, mã IL vẫn còn phụ thuộc vào trình biên dịch, do đó khẳng định rằng sẽ không có sự khác biệt ở mức IL không hoàn toàn đúng trong quan điểm kỹ thuật (mặc dù tôi sẽ nói nó là 99,9% bên phải). Dù sao tôi tin rằng hầu hết các lập trình viên sane và giàu kinh nghiệm sẽ chọn để viết như trường hợp 2 mà không có bất kỳ do dự. – tia

+1

@tia, Hiển thị IL là bằng chứng cho thấy nó có thể giống nhau và chứng minh rằng nó giống với các triển khai hiện tại. Nó không phải là bằng chứng rằng nó * nên * giống nhau, nhưng vì mã C# tương đương với bất kỳ sự khác biệt nào sẽ là một lỗ hổng nhỏ trong trình biên dịch (trong việc tạo ra mã ít hiệu quả hơn cho dù hiệu quả kém). Hai điều này với nhau (rằng nó * nên * giống nhau, và rằng nó * là * giống nhau) hoàn thành câu trả lời. –

3

Nó không quan trọng, nó không ảnh hưởng đến hiệu suất.

nhưng tôi thực sự muốn biết làm đúng cách.

Hầu hết sẽ cho bạn biết bên trong vòng lặp có ý nghĩa nhất.

3

Đó chỉ là vấn đề về phạm vi. Trong trường hợp này, trong đó foo và trừu kêu chỉ được sử dụng trong vòng lặp for, cách tốt nhất là khai báo chúng bên trong vòng lặp. Nó cũng an toàn hơn.

0

Cả hai đều hoàn toàn hợp lệ, không chắc chắn có 'cách đúng' để thực hiện việc này.

Trường hợp đầu tiên của bạn hiệu quả về bộ nhớ hơn (ít nhất là trong thời gian ngắn). Khai báo các biến của bạn bên trong vòng lặp sẽ buộc tái phân bổ bộ nhớ nhiều hơn; tuy nhiên, với bộ thu gom rác .NETs, ​​vì các biến đó nằm ngoài phạm vi chúng sẽ được dọn dẹp định kỳ, nhưng không nhất thiết phải ngay lập tức. Sự khác biệt về tốc độ được cho là không đáng kể.

Trường hợp thứ hai thực sự an toàn hơn một chút, vì việc hạn chế phạm vi các biến của bạn càng nhiều càng tốt thì thường là thực hành tốt.

+0

Mô tả của bạn về GC là sai.Trong cả hai trường hợp, nếu biến1Type và variable2Type không còn được sử dụng, thì đối tượng mà chúng trỏ đến sẽ đủ điều kiện để thu thập hoặc khi phương thức trả về hoặc khi biến cục bộ khác sử dụng vị trí của chúng trên ngăn xếp được sử dụng cho biến khác. Cả hai đều giống hệt như GC. Phạm vi không có gì để làm với thu gom rác thải. Phạm vi ảnh hưởng đến những gì bạn * có thể làm với một biến, trình biên dịch sẽ sử dụng bộ nhớ ngăn xếp tùy thuộc vào những gì bạn đã làm, và GC có thể yêu cầu bất cứ điều gì mà không có một tham chiếu trực tiếp đến nó. –

0

Trong JS phân bổ bộ nhớ là toàn bộ mỗi lần, Trong C#, không có sự khác biệt như vậy thường, nhưng nếu biến cục bộ được bắt bởi một phương thức nặc danh như biểu thức lambda thì nó sẽ quan trọng.

1

OK, tôi đã trả lời điều này mà không nhận thấy nơi áp phích ban đầu đang tạo đối tượng mới mỗi lần đi qua vòng lặp và không chỉ 'sử dụng' đối tượng. Vì vậy, không, không nên thực sự là một sự khác biệt không đáng kể khi nói đến hiệu suất. Với điều này, tôi sẽ đi với phương pháp thứ hai và khai báo đối tượng trong vòng lặp. Bằng cách đó, nó sẽ được dọn sạch trong lần truy cập GC tiếp theo và duy trì đối tượng trong phạm vi.

--- Tôi sẽ bỏ lại câu trả lời căn bản của mình, chỉ vì tôi đã nhập tất cả câu trả lời và nó có thể giúp người khác tìm kiếm thư này sau này. Tôi hứa rằng trong tương lai, tôi sẽ chú ý hơn trước khi tôi thử và trả lời câu hỏi tiếp theo.

Tim

Thực ra, tôi nghĩ có sự khác biệt. Nếu tôi nhớ chính xác, mỗi khi bạn tạo một đối tượng mới = new foo() bạn sẽ nhận được đối tượng đó được thêm vào bộ nhớ heap. Vì vậy, bằng cách tạo các đối tượng trong vòng lặp, bạn sẽ được thêm vào chi phí của hệ thống. Nếu bạn biết rằng vòng lặp sẽ nhỏ, nó không phải là một vấn đề.

Vì vậy, nếu bạn kết thúc bằng một vòng lặp với 1000 đối tượng trong đó, bạn sẽ tạo 1000 biến sẽ không được xử lý cho đến khi thu gom rác tiếp theo. Bây giờ nhấn vào một cơ sở dữ liệu nơi bạn muốn làm điều gì đó và bạn có 20.000 hàng để làm việc với ... Bạn có thể tạo ra một yêu cầu hệ thống tùy thuộc vào kiểu đối tượng bạn đang tạo.

Điều này sẽ dễ dàng kiểm tra ... Tạo ứng dụng tạo 10.000 mục có dấu thời gian khi bạn nhập vòng lặp và khi bạn thoát. Lần đầu tiên bạn làm điều đó, khai báo biến trước vòng lặp và lần tiếp theo trong vòng lặp. Bạn có thể phải tăng số lượng lên cao hơn 10K để thấy sự khác biệt thực sự về tốc độ.

Đồng thời, có vấn đề về phạm vi. Nếu được tạo trong vòng lặp, nó sẽ biến mất khi bạn thoát khỏi vòng lặp, do đó bạn không thể truy cập lại nó. Nhưng điều này cũng giúp với việc làm sạch như bộ sưu tập rác cuối cùng sẽ vứt bỏ nó một khi nó thoát khỏi vòng lặp.

Tim

+0

Biến không gây ra bộ nhớ trong heap, đối tượng làm. Họ đang tạo cùng một số đối tượng mỗi lần. –

+0

Biến có phải là một đối tượng không? Tôi thấy rằng một biến chuỗi là một đối tượng Class system.string. Vì vậy, khi bạn làm điều gì đó như "VariableType1 foo = new VariableType1();" bạn không tạo ra nó như một vật thể phải được đặt trong bộ nhớ? –

+0

Không. Biến 'string x;' trên ngăn xếp cho đối tượng được gán cho, nhưng không có đối tượng thực trên heap. 'x = chuỗi mới ('a', 23);' bây giờ có một đối tượng trên heap. 'string a = x; chuỗi b = x; chuỗi c = x; string d = x; '4 biến khác, nhưng chỉ một đối tượng trên heap. 'a = b = c = d = x = chuỗi mới ('b', 2);' một hoặc hai đối tượng trên heap như chuỗi đầu tiên sẽ là GC'd tại một số điểm, nhưng chúng ta không biết chính xác khi nào. Trong câu hỏi, cả hai dạng gọi là 'new Foo() 'và' new Baa() 'cùng số lần. Nó sử dụng bộ nhớ heap. –

Các vấn đề liên quan