Sự khác biệt về hiệu suất cho các cấu trúc điều khiển 'for' và 'foreach' trong C #


105

Đoạn mã nào sẽ cho hiệu suất tốt hơn? Các đoạn mã dưới đây được viết bằng 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, 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
Trừ khi ứng dụng của bạn rất quan trọng về hiệu suất, tôi sẽ không lo lắng về điều này. Tốt hơn nhiều để có mã rõ ràng và dễ hiểu.
Fortyrunner

2
Tôi lo lắng rằng một số câu trả lời ở đây dường như được đăng bởi những người đơn giản là không có khái niệm về trình lặp ở bất kỳ đâu trong bộ não của họ, và do đó không có khái niệm về người điều tra hoặc con trỏ.
Ed James

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

1
Mã đầu tiên cũng sẽ không biên dịch, trừ khi loại listthực sự có một countthành viên thay vì Count.
Jon Skeet

Câu trả lời:


130

Vâng, nó một phần 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.

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

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

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

CHỈNH SỬA: Đối với những người bạn đang tuyên bố rằng việc lặp qua a List<T>với foreachtạo ra mã giống như forvò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);
    }
}

Sản xuất 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 theo cách khác nhau, chuyển đổi một foreachvòng về cơ bản thành một forvòng lặp, nhưng không List<T>. Đây là mã 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

Thật thú vị, tôi không thể tìm thấy điều này được ghi lại trong thông số kỹ thuật C # 3 ở bất kỳ đâu ...


Jon không quan tâm, kịch bản với Danh sách <T> ở 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ó ý định ác ý gì) ... như trong .. bạn đã thực sự tình cờ gặp điều này trong khi cố gắng trả lời câu hỏi này, trước đó một thời gian? Đó là như vậy ... ngẫu nhiên / secret :)
Pure.Krome

5
Tôi đã nhận thức được sự tối ưu của mảng trong một thời gian - mảng là một loại tập hợp "cốt lõi"; trình biên dịch C # đã nhận thức sâu sắc về chúng, vì vậy sẽ có lý khi nó xử lý chúng theo cách khác. 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ề List<T>.
Jon Skeet

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

3
@JonSkeet Tối ưu hóa trình lặp danh sách sẽ thay đổi hành vi khi danh sách được sửa đổi trong quá trình lặp. Bạn mất ngoại lệ-nếu-sửa đổi. Vẫn có thể tối ưu hóa, nhưng yêu cầu kiểm tra xem không có sửa đổi nào xảy ra (bao gồm cả trên các luồng khác, tôi giả sử).
Craig Gidney

5
@VeeKeyBee: Microsoft đã nói như vậy vào năm 2004. a) mọi thứ thay đổi; b) công việc sẽ phải thực hiện một lượng nhỏ công việc trên mỗi lần lặp để điều này là đáng kể. Lưu ý rằng foreachtrên một mảng tương đương với foranyway. Luôn viết mã cho khả năng dễ đọc trước, sau đó chỉ tối ưu hóa vi mô khi bạn có bằng chứng rằng nó mang lại lợi ích hiệu suất có thể đo lường được.
Jon Skeet

15

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

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

Khi một foreachvòng lặp được biên dịch thành mã gần 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
        }
    }
}

Vì vậy, như bạn có thể thấy, tất cả sẽ phụ thuộc vào cách trình điều tra được triển khai so với cách trình lập chỉ mục danh sách được triển khai. Hóa ra bảng liệt kê cho các kiểu dựa trên mảng thường được viết như sau:

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

Vì vậy, như bạn có thể thấy, trong trường hợp này, nó sẽ không tạo ra nhiều khác biệt, tuy nhiên, trình điều tra cho một danh sách được liên kết có thể trông giống như sau:

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 lớp LinkedList <T> thậm chí không có trình chỉ mục, vì vậy bạn sẽ không thể thực hiện vòng lặp for của mình trên danh sách được liên kết; nhưng nếu bạn có thể, chỉ mục sẽ phải được viết như vậy:

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 có thể thấy, việc gọi điều này nhiều lần trong một vòng lặp sẽ chậm hơn nhiều so với việc sử dụng một điều tra viên có thể nhớ vị trí của nó trong danh sách.


12

Một thử nghiệm dễ dàng để xác nhận bán hợp lệ. 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ế for bằng foreach - foreach nhanh hơn 20 mili giây - nhất quán . Đối với là 135-139ms trong khi khoảng trước là 113-119ms. Tôi đã trao đổi qua lại nhiều lần, đảm bảo rằng đó không phải là một quá trình nào đó mới bắt đầu.

Tuy nhiên, khi tôi loại bỏ lệnh foo và if, lệnh for nhanh hơn 30 ms (foreach là 88ms và for là 59ms). Cả hai đều là vỏ rỗng. Tôi giả sử foreach thực sự đã truyền một biến trong đó for chỉ tăng một biến. Nếu tôi đã thêm

int foo = intList[i];

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

Thành thật mà nói .. Tôi mong đợi 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 các đề xuất của Jons (134217728 là int lớn nhất mà bạn có thể có trước khi ngoại lệ System.OutOfMemory được ném):

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ả:

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

Trao đổi chúng xung quanh để xem liệu nó có xử lý theo thứ tự của mọi thứ hay không mang lại kết quả giống nhau (gần như).


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

8
Các 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 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 (để lấy list.count) Nói tóm lại, bạn đang đo điểm chuẩn cho hai đoạn mã khác nhau, do đó kết quả của bạn khá lạ. Hãy thử 'int max = intlist.Count; for (int i = 0; i <max; i ++) ... 'và vòng lặp' for 'sẽ luôn chạy nhanh hơn, như mong đợi!
AR

1
Sau khi biên dịch, for và foreach tối ưu hóa đến cùng một thứ khi làm việc với nguyên thủy. Cho đến khi bạn giới thiệu Danh sách <T> thì chúng mới khác nhau (rất nhiều) về tốc độ.
Anthony Russell

9

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

Nếu listbạn đang làm việc là a LinkedList, thì hiệu suất của mã lập chỉ mục ( truy cập kiểu mảng ) kém hơn rất nhiều so với việc sử dụng IEnumeratorfrom foreach, cho các danh sách lớn.

Khi bạn truy cập phần tử 10.000 trong một LinkedListbằng cú pháp của trình lập chỉ mục:, list[10000]danh sách được liên kết sẽ bắt đầu ở nút đầu và lướt 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(mặc nhiên sử dụng forach-syntax), bạn sẽ nhận được một IEnumeratorđối tượng có một con trỏ đến nút đầu. Mỗi lần bạn gọi MoveNext, con trỏ đó được di chuyển đến nút tiếp theo, như sau:

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 LinkedLists, phương thức chỉ mục mảng càng ngày càng chậm, bạn lặp lại càng lâu (nó phải đi qua cùng một con trỏ đầu nhiều lần). Trong khi IEnumerablechỉ 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 listkhông phải là a LinkedList, mà là một mảng, thì hành vi hoàn toàn khác nhau.


4
LinkedList trong .NET không có trình chỉ mục, vì vậy nó thực sự không phải là một tùy chọn.
Jon Skeet, 14/07/09

Ồ, nó giải quyết được vấn đề đó, sau đó :-) Tôi chỉ đang xem qua các LinkedList<T>tài liệu về MSDN và nó có một API khá tốt. Quan trọng nhất, nó không có get(int index)phương thức, giống như Java. Tuy nhiên, tôi đoán vấn đề vẫn còn tồn tại đối với bất kỳ cấu trúc dữ liệu giống danh sách nào khác hiển thị một trình lập chỉ mục chậm hơn một trình cụ thể IEnumerator.
Tom Lokhorst, 14/07/09

2

Giống như những người khác đã đề cập, mặc dù hiệu suất thực sự không quan trọng lắm, foreach sẽ luôn chậm hơn một chút do sử dụng IEnumerable/ IEnumeratortrong vòng lặp. Trình biên dịch chuyển cấu trúc thành các lệnh gọi trên giao diện đó và đối với 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à sự 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ể khác nhau như thế nào dựa trên việc triển khai MoveNext và Current. Trong khi trong một quyền truy cập mảng, bạn không có sự phụ thuộc đó.


4
Đừng quên có sự khác biệt giữa quyền truy cập mảng và quyền truy cập trình chỉ mục. Nếu danh sách là một List<T>ở đây thì vẫn có lần truy cập (có thể là nội tuyến) gọi trình chỉ mục. Nó không giống như một truy cập mảng kim loại trần.
Jon Skeet

Rất đúng! Nó lại là một hành động tài sản khác và chúng tôi đang thực hiện.
Charles Prakash Dasari 14/07/09

1

Sau khi đọc đủ các lập luận rằng "vòng lặp foreach nên được ưu tiên hơn để dễ đọc", tôi có thể nói rằng phản ứng đầu tiên của tôi là "cái gì"? Nói chung, khả năng đọc là chủ quan và trong trường hợp cụ thể này, thậm chí còn hơn thế nữa. Đối với người có kiến ​​thức nền tảng về lập trình (thực tế, mọi ngôn ngữ trước Java), vòng lặp for dễ đọc hơn nhiều so với vòng lặp foreach. Ngoài ra, những người cùng 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à khó bảo trì, điều này đã chứng minh cho quan đ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 này .

CHỈNH SỬA: Có những bộ sưu tập trong C # (như HashSet) không có bộ lập chỉ mục. Trong những bộ sưu tập, foreach là cách duy nhất để lặp và nó phải là trường hợp duy nhất mà tôi nghĩ rằng nó nên được sử dụng qua cho .


0

Có một sự thật thú vị khác có thể dễ dàng bỏ qua khi kiểm tra tốc độ của cả hai vòng lặp: 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ị rằng foreach nhanh hơn so với ở chế độ gỡ lỗi. Trong khi for ist nhanh hơn foreach trong chế độ phát hành. Rõ ràng là 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 làm ảnh hưởng đến một số lời gọi phương thức. Vòng lặp for về mặt cơ bản đến mức có thể điều này thậm chí còn được tối ưu hóa bởi chính CPU.


0

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

Cấu foreachtrúc tiêu chuẩn có thể nhanh hơn (1,5 chu kỳ mỗi bước) so với cấu trúc đơn giản for-loop(2 chu kỳ mỗi bước), trừ khi vòng lặp chưa được cuộn (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à một lý do để sử dụng phức tạp hơn for, whilehoặc do-whilecấu trúc.

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ã mà bạn đã liên kết. Đó là một bài báo thú vị, nhưng nó nói hoàn toàn ngược lại với bài viết của bạn. Ngoài ra, bảng bạn đã tạo lại đang đo lường hiệu suất của việc truy cập trực tiếp vào một mảng và Danh sách hoặc thông qua các giao diện IList của chúng. Không liên quan gì đến câu hỏi. :)
Paul Walls

0

bạn có thể đọc về nó trong Deep .NET - phần 1 Lặp lại

nó bao gồm các kết quả (không có lần khởi tạo đầu tiên) từ mã nguồn .NET đến quá trình tháo gỡ.

ví dụ - Lặp lại mảng với vòng lặp foreach: nhập mô tả hình ảnh ở đây

và - lặp lại danh sách với vòng lặp foreach: nhập mô tả hình ảnh ở đây

và kết quả cuối cùng: nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

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.