Cuộc thảo luận về sự khác biệt ưu tiên giữa FOREACH và FOR không phải là mới. Tất cả chúng ta đều biết rằng FOREACH chậm hơn, nhưng không phải tất cả đều biết tại sao.
Khi tôi bắt đầu học .NET, một người đã nói với tôi rằng FOREACH chậm hơn FOR hai lần. Anh ta nói điều này mà không có bất kỳ căn cứ nào. Tôi coi đó là điều hiển nhiên.
Cuối cùng, tôi quyết định khám phá sự khác biệt về hiệu suất vòng lặp FOREACH và vòng lặp FOR, và viết bài báo này để thảo luận về các sắc thái.
Hãy xem đoạn mã sau:
foreach (var item in Enumerable.Range(0, 128))
{
Console.WriteLine(item);
}
FOREACH là một đường cú pháp. Trong trường hợp cụ thể này, trình biên dịch chuyển nó thành đoạn mã sau:
IEnumerator<int> enumerator = Enumerable.Range(0, 128).GetEnumerator();
try
{
while (enumerator.MoveNext())
{
int item = enumerator.Current;
Console.WriteLine(item);
}
}
finally
{
if (enumerator != null)
{
enumerator.Dispose();
}
}
Biết được điều này, chúng ta có thể giả định lý do FOREACH chậm hơn FOR:
- Một đối tượng mới đang được tạo. Nó được gọi là Người sáng tạo.
- Phương thức MoveNext được gọi trong mỗi lần lặp.
- Mỗi lần lặp lại truy cập thuộc tính Hiện tại.
Đó là nó! Tuy nhiên, nó không phải là tất cả dễ dàng như bạn tưởng.
May mắn thay (hoặc không may), C # / CLR có thể thực hiện tối ưu hóa tại thời điểm chạy. Điểm chuyên nghiệp là mã hoạt động nhanh hơn. Các nhà phát triển lừa đảo nên biết về những tối ưu hóa này.
Mảng là một kiểu được tích hợp sâu vào CLR và CLR cung cấp một số tối ưu hóa cho kiểu này. Vòng lặp FOREACH là một thực thể có thể lặp lại, là một khía cạnh chính của hiệu suất. Ở phần sau của bài viết, chúng ta sẽ thảo luận về cách lặp qua các mảng và danh sách với sự trợ giúp của phương thức tĩnh Array.ForEach và phương thức List.ForEach.
Phương pháp thử nghiệm
static double ArrayForWithoutOptimization(int[] array)
{
int sum = 0;
var watch = Stopwatch.StartNew();
for (int i = 0; i < array.Length; i++)
sum += array[i];
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
static double ArrayForWithOptimization(int[] array)
{
int length = array.Length;
int sum = 0;
var watch = Stopwatch.StartNew();
for (int i = 0; i < length; i++)
sum += array[i];
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
static double ArrayForeach(int[] array)
{
int sum = 0;
var watch = Stopwatch.StartNew();
foreach (var item in array)
sum += item;
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
static double ArrayForEach(int[] array)
{
int sum = 0;
var watch = Stopwatch.StartNew();
Array.ForEach(array, i => { sum += i; });
watch.Stop();
return watch.Elapsed.TotalMilliseconds;
}
Điều kiện thử nghiệm:
- Tùy chọn "Tối ưu hóa mã" đã được bật.
- Số phần tử bằng 100 000 000 (cả trong mảng và danh sách).
- Thông số kỹ thuật của PC:Intel Core i-5 và 8 GB RAM.
Mảng
Biểu đồ cho thấy FOR và FOREACH dành cùng một khoảng thời gian trong khi lặp qua các mảng. Và đó là vì tối ưu hóa CLR chuyển FOREACH thành FOR và sử dụng độ dài của mảng làm ranh giới lặp lại tối đa. Không quan trọng độ dài mảng có được lưu vào bộ nhớ đệm hay không (khi sử dụng FOR), kết quả gần như giống nhau.
Nghe có vẻ lạ, nhưng bộ nhớ đệm chiều dài mảng có thể ảnh hưởng đến hiệu suất. Trong khi sử dụng mảng .Length là ranh giới lặp lại, JIT kiểm tra chỉ số để đạt đến đường biên bên phải ngoài chu kỳ. Việc kiểm tra này chỉ được thực hiện một lần.
Nó rất dễ dàng để phá hủy sự tối ưu hóa này. Trường hợp khi biến được lưu trong bộ nhớ cache hầu như không được tối ưu hóa.
Array.foreach cho thấy kết quả tồi tệ nhất. Việc triển khai nó khá đơn giản:
public static void ForEach<T>(T[] array, Action<T> action)
{
for (int index = 0; index < array.Length; ++index)
action(array[index]);
}
Sau đó, tại sao nó chạy quá chậm? Nó sử dụng FOR dưới mui xe. Chà, lý do là trong việc gọi đại biểu ACTION. Trên thực tế, một phương thức được gọi trên mỗi lần lặp, điều này làm giảm hiệu suất. Hơn nữa, các đại biểu được gọi không nhanh như chúng tôi mong muốn.
Danh sách
Kết quả là hoàn toàn khác. Khi lặp lại danh sách, FOR và FOREACH hiển thị các kết quả khác nhau. Không có tối ưu hóa. FOR (với độ dài của danh sách trong bộ nhớ đệm) cho kết quả tốt nhất, trong khi FOREACH chậm hơn 2 lần. Đó là bởi vì nó xử lý MoveNext và Hiện tại dưới mui xe. List.ForEach cũng như Array.ForEach cho thấy kết quả tồi tệ nhất. Các đại biểu luôn được gọi là ảo. Việc triển khai phương pháp này trông giống như sau:
public void ForEach(Action<T> action)
{
int num = this._version;
for (int index = 0; index < this._size && num == this._version; ++index)
action(this._items[index]);
if (num == this._version)
return;
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
Mỗi lần lặp gọi đại biểu Hành động. Nó cũng kiểm tra xem danh sách có bị thay đổi hay không và nếu có, một ngoại lệ sẽ được đưa ra.
Liệt kê nội bộ sử dụng mô hình dựa trên mảng và phương thức ForEach sử dụng chỉ số mảng để lặp lại, nhanh hơn đáng kể so với việc sử dụng trình chỉ mục.
Các con số cụ thể
- Vòng lặp FOR không có bộ nhớ đệm độ dài và FOREACH hoạt động trên các mảng nhanh hơn một chút so với FOR có bộ nhớ đệm độ dài.
- Mảng. Foreach hiệu suất chậm hơn khoảng 6 lần so với hiệu suất FOR / FOREACH.
- Vòng lặp FOR không có bộ nhớ đệm độ dài hoạt động chậm hơn 3 lần trên danh sách, so với các mảng.
- Vòng lặp FOR với bộ nhớ đệm độ dài hoạt động chậm hơn 2 lần trên danh sách, so với các mảng.
- Vòng lặp FOREACH hoạt động chậm hơn 6 lần trên danh sách, so với các mảng.
Đây là bảng lãnh đạo cho các danh sách:
Và đối với mảng:
Kết luận
Tôi thực sự thích cuộc điều tra này, đặc biệt là quá trình viết, và tôi hy vọng bạn cũng thích nó. Hóa ra, FOREACH nhanh hơn trên mảng so với FOR với tính năng đuổi theo độ dài. Trên cấu trúc danh sách, FOREACH chậm hơn FOR.
Mã trông đẹp hơn khi sử dụng FOREACH và các bộ xử lý hiện đại cho phép sử dụng mã này. Tuy nhiên, nếu bạn cần tối ưu hóa cơ sở mã của mình, tốt hơn nên sử dụng FOR.
Bạn nghĩ sao, vòng lặp nào chạy nhanh hơn, FOR hay FOREACH?