2009-07-14 27 views
100

Đoạn mã nào sẽ mang lại hiệu suất tốt hơn? Các đoạn mã dưới đây được viết bằng C#.Sự khác biệt hiệu suất đối với cấu trúc điều khiển 'cho' và 'foreach' trong C#

1.

for(int counter=0; counter<list.Count; counter++) 
{ 
    list[counter].DoSomething(); 
} 

2.

foreach(MyType current in list) 
{ 
    current.DoSomething(); 
} 
+31

tôi tưởng tượng rằng nó không thực sự quan trọng. Nếu bạn đang gặp vấn đề về hiệu suất, nó gần như chắc chắn không phải do điều này. Không phải là bạn không nên đặt câu hỏi ... – darasd

+2

Nó lo lắng cho tôi rằng một số câu trả lời ở đây dường như được đăng bởi những người chỉ đơn giản là không có khái niệm về một vòng lặp bất cứ nơi nào trong não của họ, và do đó không có khái niệm về điều tra viên hoặc con trỏ. –

+3

Mã thứ hai đó sẽ không biên dịch. System.Object không có thành viên nào được gọi là 'value' (trừ khi bạn thực sự xấu xa, đã định nghĩa nó như là một phương thức mở rộng và đang so sánh các đại biểu). Mạnh mẽ loại của bạn foreach. – Trillian

Trả lời

126

Vâng, nó phần nào phụ thuộc vào loại chính xác của list. Nó cũng sẽ phụ thuộc vào CLR chính xác mà bạn đang sử dụng.

Dù theo bất kỳ cách nào, đáng kể hoặc không phụ thuộc vào việc bạn có đang thực hiện bất kỳ công việc thực nào trong vòng lặp hay không. Trong hầu hết các trường hợp tất cả, sự khác biệt về hiệu suất sẽ không đáng kể, nhưng sự khác biệt đối với khả năng đọc sẽ ủng hộ vòng lặp foreach.

Cá nhân tôi muốn sử dụng LINQ để tránh "nếu" quá:

foreach (var item in list.Where(condition)) 
{ 
} 

EDIT: Đối với những người bạn của những người tuyên bố rằng iterating trên một List<T> với foreach sản xuất mã giống như for vòng lặp, đây là bằng chứng cho thấy nó không:

static void IterateOverList(List<object> list) 
{ 
    foreach (object o in list) 
    { 
     Console.WriteLine(o); 
    } 
} 

Tạo IL của:

.method private hidebysig static void IterateOverList(class [mscorlib]System.Collections.Generic.List`1<object> list) cil managed 
{ 
    // Code size  49 (0x31) 
    .maxstack 1 
    .locals init (object V_0, 
      valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object> V_1) 
    IL_0000: ldarg.0 
    IL_0001: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<object>::GetEnumerator() 
    IL_0006: stloc.1 
    .try 
    { 
    IL_0007: br.s  IL_0017 
    IL_0009: ldloca.s V_1 
    IL_000b: call  instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::get_Current() 
    IL_0010: stloc.0 
    IL_0011: ldloc.0 
    IL_0012: call  void [mscorlib]System.Console::WriteLine(object) 
    IL_0017: ldloca.s V_1 
    IL_0019: call  instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::MoveNext() 
    IL_001e: brtrue.s IL_0009 
    IL_0020: leave.s IL_0030 
    } // end .try 
    finally 
    { 
    IL_0022: ldloca.s V_1 
    IL_0024: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object> 
    IL_002a: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
    IL_002f: endfinally 
    } // end handler 
    IL_0030: ret 
} // end of method Test::IterateOverList 

Trình biên dịch xử lý các mảng khác nhau, chuyển đổi vòng lặp foreach về cơ bản thành vòng lặp for, nhưng không phải là List<T>. Dưới đây là đoạn code tương đương cho một mảng:

static void IterateOverArray(object[] array) 
{ 
    foreach (object o in array) 
    { 
     Console.WriteLine(o); 
    } 
} 

// Compiles into... 

.method private hidebysig static void IterateOverArray(object[] 'array') cil managed 
{ 
    // Code size  27 (0x1b) 
    .maxstack 2 
    .locals init (object V_0, 
      object[] V_1, 
      int32 V_2) 
    IL_0000: ldarg.0 
    IL_0001: stloc.1 
    IL_0002: ldc.i4.0 
    IL_0003: stloc.2 
    IL_0004: br.s  IL_0014 
    IL_0006: ldloc.1 
    IL_0007: ldloc.2 
    IL_0008: ldelem.ref 
    IL_0009: stloc.0 
    IL_000a: ldloc.0 
    IL_000b: call  void [mscorlib]System.Console::WriteLine(object) 
    IL_0010: ldloc.2 
    IL_0011: ldc.i4.1 
    IL_0012: add 
    IL_0013: stloc.2 
    IL_0014: ldloc.2 
    IL_0015: ldloc.1 
    IL_0016: ldlen 
    IL_0017: conv.i4 
    IL_0018: blt.s  IL_0006 
    IL_001a: ret 
} // end of method Test::IterateOverArray 

Điều thú vị là, tôi không thể tìm thấy điều này được ghi lại trong C# 3 đặc tả bất cứ nơi nào ...

+0

Không quan tâm Jon, kịch bản với Danh sách ở trên ... có áp dụng cho các bộ sưu tập khác không? Ngoài ra, làm thế nào bạn biết điều này (không có ác ý định gì) ... như trong .. đã u nghĩa đen vấp ngã này trong khi cố gắng trả lời câu hỏi này, trước đây một số thời gian trước đây? Thật là ... ngẫu nhiên/bí mật :) –

+5

Tôi đã biết về sự tối ưu hóa mảng trong một thời gian - mảng là một loại bộ sưu tập "cốt lõi"; trình biên dịch C# đã nhận thức sâu sắc về chúng, do đó, nó có ý nghĩa đối với nó để đối xử với chúng một cách khác nhau. Trình biên dịch không (và không nên) có bất kỳ kiến ​​thức đặc biệt nào về 'Danh sách '. –

+0

Chúc mừng :) và vâng ... mảng là khái niệm bộ sưu tập đầu tiên tôi đã được dạy nhiều năm trước đây tại uni .. vì vậy nó sẽ làm cho sence rằng trình biên dịch là đủ thông minh để đối phó với một trong (nếu không phải là) nguyên thủy nhất loại của bộ sưu tập. cổ vũ một lần nữa! –

2

Giống như những người khác đã đề cập đến mặc dù việc thực hiện không thực sự quan trọng nhiều, foreach sẽ luôn chậm hơn một chút do sử dụng IEnumerable/IEnumerator trong vòng lặp. Trình biên dịch dịch cấu trúc thành các cuộc gọi trên giao diện đó và cho mỗi bước một hàm + một thuộc tính được gọi trong cấu trúc foreach.

IEnumerator iterator = ((IEnumerable)list).GetEnumerator(); 
while (iterator.MoveNext()) { 
    var item = iterator.Current; 
    // do stuff 
} 

Đây là bản mở rộng tương đương của cấu trúc trong C#. Bạn có thể tưởng tượng tác động hiệu suất có thể thay đổi như thế nào dựa trên việc triển khai MoveNext và Current. Trong khi truy cập mảng, bạn không có phụ thuộc đó.

+4

Đừng quên có sự khác biệt giữa truy cập mảng và truy cập chỉ mục. Nếu danh sách là một 'Danh sách ' ở đây thì vẫn có cú đánh (có thể được gạch chân) gọi đến trình chỉ mục.Nó không giống như là một mảng kim loại trần. –

+0

Rất đúng! Đó là một thực hiện tài sản khác và chúng tôi đang ở lòng thương xót của việc thực hiện. –

12

Một thử nghiệm dễ dàng để bán xác thực. Tôi đã làm một bài kiểm tra nhỏ, chỉ để xem. Đây là mã:

static void Main(string[] args) 
{ 
    List<int> intList = new List<int>(); 

    for (int i = 0; i < 10000000; i++) 
    { 
     intList.Add(i); 
    } 

    DateTime timeStarted = DateTime.Now; 
    for (int i = 0; i < intList.Count; i++) 
    { 
     int foo = intList[i] * 2; 
     if (foo % 2 == 0) 
     { 
     } 
    } 

    TimeSpan finished = DateTime.Now - timeStarted; 

    Console.WriteLine(finished.TotalMilliseconds.ToString()); 
    Console.Read(); 

} 

Và đây là phần foreach:

foreach (int i in intList) 
{ 
    int foo = i * 2; 
    if (foo % 2 == 0) 
    { 
    } 
} 

Khi tôi thay thế cho một foreach - foreach là 20 mili giây nhanh hơn - luôn. Cho là 135-139ms trong khi foreach là 113-119ms. Tôi đổi chỗ qua lại nhiều lần, chắc chắn rằng đó không phải là một quá trình nào đó vừa mới khởi động.

Tuy nhiên, khi tôi xóa câu lệnh if và if, giá trị nhanh hơn 30 ms (foreach là 88ms và là 59ms). Cả hai đều là những cái vỏ rỗng. Tôi giả định foreach thực sự đã thông qua một biến nơi mà cho là chỉ tăng một biến. Nếu tôi đã thêm

int foo = intList[i]; 

Sau đó, cho trở nên chậm khoảng 30ms. Tôi giả định điều này đã làm với nó tạo foo và lấy biến trong mảng và gán nó cho foo. Nếu bạn chỉ cần truy cập intList [i] thì bạn không có hình phạt đó.

Trong tất cả sự trung thực .. Tôi dự kiến ​​việc foreach sẽ chậm hơn một chút trong mọi trường hợp, nhưng không đủ để quan trọng trong hầu hết các ứng dụng.

chỉnh sửa: đây là mã mới sử dụng gợi ý Jons (134217728 là lớn nhất int bạn có thể có trước khi ngoại lệ được ném System.OutOfMemory):

static void Main(string[] args) 
{ 
    List<int> intList = new List<int>(); 

    Console.WriteLine("Generating data."); 
    for (int i = 0; i < 134217728 ; i++) 
    { 
     intList.Add(i); 
    } 

    Console.Write("Calculating for loop:\t\t"); 

    Stopwatch time = new Stopwatch(); 
    time.Start(); 
    for (int i = 0; i < intList.Count; i++) 
    { 
     int foo = intList[i] * 2; 
     if (foo % 2 == 0) 
     { 
     } 
    } 

    time.Stop(); 
    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms"); 
    Console.Write("Calculating foreach loop:\t"); 
    time.Reset(); 
    time.Start(); 

    foreach (int i in intList) 
    { 
     int foo = i * 2; 
     if (foo % 2 == 0) 
     { 
     } 
    } 

    time.Stop(); 

    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms"); 
    Console.Read(); 
} 

Và đây là kết quả:

Tạo dữ liệu. Tính toán vòng lặp: 2458ms Tính toán vòng lặp foreach: 2005ms

Hoán đổi xung quanh để xem liệu nó có khớp với thứ tự kết quả có cùng kết quả hay không (gần).

+6

Tốt hơn nên sử dụng Đồng hồ bấm giờ hơn DateTime.Now - và tôi sẽ không tin tưởng bất kỳ hoạt động nào nhanh chóng, thành thật. –

+8

Vòng lặp foreach của bạn đang chạy nhanh hơn vì 'for' đánh giá điều kiện mỗi lần lặp. Trong trường hợp của ví dụ của bạn, điều này làm cho một cuộc gọi phương thức bổ sung (để có được list.count) Tóm lại, bạn đang chuẩn hóa hai phần mã khác nhau, do đó kết quả lạ của bạn. Hãy thử 'int max = intlist.Count; cho (int i = 0; i

+1

Sau khi biên soạn, cho và foreach tối ưu hóa chính xác cùng một điều khi làm việc với nguyên thủy. Nó không phải là cho đến khi bạn giới thiệu Danh sách Rằng chúng khác nhau (rất nhiều) về tốc độ. –

9

Lưu ý: câu trả lời này áp dụng nhiều hơn cho Java so với C#, vì C# không có chỉ mục trên LinkedLists, nhưng tôi nghĩ điểm chung vẫn giữ.

Nếu list bạn đang làm việc với sẽ xảy ra là một LinkedList, hiệu suất của indexer-code (mảng kiểu việc truy cập) là tồi tệ hơn rất nhiều so với sử dụng IEnumerator từ foreach, cho các danh sách lớn.

Khi bạn truy cập phần tử 10.000 trong một LinkedList sử dụng cú pháp chỉ mục: list[10000], danh sách được liên kết sẽ bắt đầu tại nút đầu và đi qua Next -pointer mười nghìn lần, cho đến khi nó đến đúng đối tượng. Rõ ràng, nếu bạn làm điều này trong một vòng lặp, bạn sẽ nhận được:

list[0]; // head 
list[1]; // head.Next 
list[2]; // head.Next.Next 
// etc. 

Khi bạn gọi GetEnumerator (ngầm sử dụng forach -syntax), bạn sẽ nhận được một đối tượng IEnumerator rằng có một con trỏ đến nút đầu. Mỗi lần bạn gọi MoveNext, con trỏ được chuyển đến nút kế tiếp, như vậy:

IEnumerator em = list.GetEnumerator(); // Current points at head 
em.MoveNext(); // Update Current to .Next 
em.MoveNext(); // Update Current to .Next 
em.MoveNext(); // Update Current to .Next 
// etc. 

Như bạn có thể thấy, trong trường hợp của LinkedList s, phương pháp mảng indexer trở nên chậm hơn và chậm hơn, các bạn còn loop (nó phải đi qua cùng một đầu con trỏ hơn và hơn nữa). Trong khi IEnumerable chỉ hoạt động trong thời gian không đổi.

Tất nhiên, như Jon nói điều này thực sự phụ thuộc vào loại list, nếu list không phải là LinkedList, nhưng một mảng, hành vi hoàn toàn khác.

+4

LinkedList in .NET không có bộ chỉ mục, vì vậy nó không thực sự là một tùy chọn. –

+0

Ồ, điều đó giải quyết vấn đề đó, sau đó :-) Tôi chỉ xem qua các tài liệu 'LinkedList ' trên MSDN và nó có API khá phong nha. Quan trọng nhất, nó không có phương thức 'get (int index)', giống như Java. Tuy nhiên, tôi đoán điểm vẫn giữ cho bất kỳ cấu trúc dữ liệu giống như danh sách nào khác cho thấy một trình lập chỉ mục chậm hơn một trình duyệt IE cụ thể'. –

14

Một vòng lặp for được biên dịch mã tương đương với điều này:

int tempCount = 0; 
while (tempCount < list.Count) 
{ 
    if (list[tempCount].value == value) 
    { 
     // Do something 
    } 
    tempCount++; 
} 

Trong trường hợp như một vòng lặp foreach được biên dịch mã tương đương với điều này:

using (IEnumerator<T> e = list.GetEnumerator()) 
{ 
    while (e.MoveNext()) 
    { 
     T o = (MyClass)e.Current; 
     if (row.value == value) 
     { 
      // Do something 
     } 
    } 
} 

Như bạn có thể thấy, tất cả sẽ phụ thuộc vào cách điều tra viên được thực hiện so với cách trình lập chỉ mục danh sách được thực hiện. Khi nó quay ra các điều tra viên với nhiều loại dựa trên mảng thường được viết một cái gì đó như thế này:

private static IEnumerable<T> MyEnum(List<T> list) 
{ 
    for (int i = 0; i < list.Count; i++) 
    { 
     yield return list[i]; 
    } 
} 

Như bạn có thể thấy, trong trường hợp này nó sẽ không làm cho rất nhiều sự khác biệt, tuy nhiên các điều tra viên cho một danh sách liên kết có lẽ sẽ giống như thế này:

private static IEnumerable<T> MyEnum(LinkedList<T> list) 
{ 
    LinkedListNode<T> current = list.First; 
    do 
    { 
     yield return current.Value; 
     current = current.Next; 
    } 
    while (current != null); 
} 

Trong .NET bạn sẽ thấy rằng LinkedList < T> class thậm chí không có một indexer, vì vậy bạn sẽ không thể làm cho vòng lặp trên danh sách liên kết; nhưng nếu bạn có thể, indexer sẽ phải được viết như sau:

public T this[int index] 
{ 
     LinkedListNode<T> current = this.First; 
     for (int i = 1; i <= index; i++) 
     { 
      current = current.Next; 
     } 
     return current.value; 
} 

Như bạn thấy, kêu gọi này nhiều lần trong một vòng lặp sẽ là chậm hơn nhiều so với sử dụng một Enumerator có thể nhớ nó ở đâu trong danh sách.

0

Có một thực tế thú vị khác có thể dễ dàng bỏ qua khi kiểm tra tốc độ của cả hai vòng: Sử dụng chế độ gỡ lỗi không cho phép trình biên dịch tối ưu hóa mã bằng cài đặt mặc định.

Điều này dẫn tôi đến kết quả thú vị là foreach nhanh hơn trong chế độ gỡ lỗi. Trong khi đó cho ist nhanh hơn foreach trong chế độ phát hành. Rõ ràng trình biên dịch có những cách tốt hơn để tối ưu hóa vòng lặp for hơn là vòng lặp foreach, nó làm tổn hại đến một số cuộc gọi phương thức. Một vòng lặp for là bằng cách cơ bản như vậy mà nó có thể là điều này thậm chí được tối ưu hóa bởi chính CPU.

1

Sau khi đọc đủ đối số rằng "vòng lặp foreach nên được ưu tiên cho khả năng đọc", tôi có thể nói phản ứng đầu tiên của tôi là "cái gì"? Dễ đọc, nói chung, là chủ quan và, trong trường hợp cụ thể này, thậm chí nhiều hơn. Đối với người có nền tảng về lập trình (thực tế, mọi ngôn ngữ trước Java), đối với các vòng lặp dễ đọc hơn nhiều so với vòng lặp foreach. Ngoài ra, cùng một người tuyên bố rằng vòng lặp foreach dễ đọc hơn, cũng là những người ủng hộ linq và các "tính năng" khác làm cho mã khó đọc và duy trì, điều gì đó chứng minh điểm trên.

Về tác động đến hiệu suất, hãy xem câu trả lời cho câu hỏi this.

EDIT: Có các bộ sưu tập trong C# (như HashSet) không có bộ chỉ mục. Trong các bộ sưu tập này, foreach là cách duy nhất để lặp lại và đó là trường hợp duy nhất tôi cho rằng nó nên được sử dụng trên cho.

0

Trong ví dụ bạn đã cung cấp, tốt hơn hết là sử dụng vòng lặp foreach thay vì vòng lặp for.

Cấu trúc tiêu chuẩn foreach có thể nhanh hơn (1,5 chu kỳ mỗi bước) so với đơn giản for-loop (2 chu kỳ mỗi bước), trừ khi vòng lặp chưa được kiểm tra (1.0 chu kỳ mỗi bước).

Vì vậy, đối với mã hàng ngày, hiệu suất không phải là lý do để sử dụng các cấu trúc phức tạp hơn for, while hoặc do-while.

Kiểm tra liên kết này: http://www.codeproject.com/Articles/146797/Fast-and-Less-Fast-Loops-in-C


╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗ 
║  Method  ║ List<int> ║ int[] ║ Ilist<int> onList<Int> ║ Ilist<int> on int[] ║ 
╠══════════════════════╬═══════════╬═══════╬════════════════════════╬═════════════════════╣ 
║ Time (ms)   ║ 23,80  ║ 17,56 ║ 92,33     ║ 86,90    ║ 
║ Transfer rate (GB/s) ║ 2,82  ║ 3,82 ║ 0,73     ║ 0,77    ║ 
║ % Max    ║ 25,2%  ║ 34,1% ║ 6,5%     ║ 6,9%    ║ 
║ Cycles/read  ║ 3,97  ║ 2,93 ║ 15,41     ║ 14,50    ║ 
║ Reads/iteration ║ 16  ║ 16 ║ 16      ║ 16     ║ 
║ Cycles/iteration ║ 63,5  ║ 46,9 ║ 246,5     ║ 232,0    ║ 
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝ 

+4

Bạn có thể đọc lại bài viết dự án mã bạn đã liên kết. Nó là một bài viết thú vị, nhưng nó nói chính xác ngược lại bài viết của bạn. Ngoài ra, bảng bạn tạo lại là đo hiệu suất truy cập một mảng và Danh sách trực tiếp, hoặc thông qua giao diện IList của chúng. Không có bất cứ điều gì để làm với câu hỏi. :) –

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