Khi nào, nếu có, việc hủy cuộn vòng lặp vẫn hữu ích?


93

Tôi đã cố gắng tối ưu hóa một số mã cực kỳ quan trọng về hiệu suất (một thuật toán sắp xếp nhanh được gọi hàng triệu triệu lần bên trong mô phỏng monte carlo) bằng cách mở vòng lặp. Đây là vòng lặp bên trong mà tôi đang cố gắng tăng tốc:

// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}

Tôi đã thử hủy cuộn đến một cái gì đó như:

while(true) {
    if(myArray[++index1] < pivot) break;
    if(myArray[++index1] < pivot) break;
    // More unrolling
}


while(true) {
    if(pivot < myArray[--index2]) break;
    if(pivot < myArray[--index2]) break;
    // More unrolling
}

Điều này hoàn toàn không có gì khác biệt vì vậy tôi đã thay đổi nó trở lại dạng dễ đọc hơn. Tôi đã có trải nghiệm tương tự lần khác, tôi đã thử mở vòng lặp. Với chất lượng của các bộ dự đoán nhánh trên phần cứng hiện đại, khi nào, nếu có, việc giải nén vòng lặp vẫn là một tối ưu hóa hữu ích?


1
Tôi có thể hỏi tại sao bạn không sử dụng các thói quen nhanh chóng của thư viện tiêu chuẩn không?
Peter Alexander

14
@Poita: Bởi vì tôi có một số tính năng bổ sung mà tôi cần cho các tính toán thống kê mà tôi đang thực hiện và được điều chỉnh rất cao cho các trường hợp sử dụng của tôi và do đó ít tổng quát hơn nhưng nhanh hơn so với lib tiêu chuẩn. Tôi đang sử dụng ngôn ngữ lập trình D, có trình tối ưu hóa cũ kỹ và đối với các mảng lớn ngẫu nhiên, tôi vẫn đánh bại loại C ++ STL của GCC từ 10-20%.
dsimcha

Câu trả lời:


122

Bỏ cuộn vòng lặp sẽ có ý nghĩa nếu bạn có thể phá vỡ chuỗi phụ thuộc. Điều này cho phép CPU siêu vô hướng có khả năng lên lịch mọi thứ tốt hơn và do đó chạy nhanh hơn.

Một ví dụ đơn giản:

for (int i=0; i<n; i++)
{
  sum += data[i];
}

Ở đây, chuỗi phụ thuộc của các đối số rất ngắn. Nếu bạn gặp sự cố vì bạn có lỗi bộ nhớ cache trên mảng dữ liệu, cpu không thể làm gì khác ngoài việc chờ đợi.

Mặt khác, mã này:

for (int i=0; i<n; i+=4)
{
  sum1 += data[i+0];
  sum2 += data[i+1];
  sum3 += data[i+2];
  sum4 += data[i+3];
}
sum = sum1 + sum2 + sum3 + sum4;

có thể chạy nhanh hơn. Nếu bạn bị lỗi bộ nhớ cache hoặc lỗi khác trong một phép tính thì vẫn còn ba chuỗi phụ thuộc khác không phụ thuộc vào lỗi. Một CPU không hoạt động có thể thực thi những điều này.


2
Cảm ơn. Tôi đã thử mở vòng lặp theo kiểu này ở một số nơi khác trong thư viện, nơi tôi đang tính tổng và nhiều thứ, và ở những nơi này, nó hoạt động rất kỳ diệu. Tôi gần như chắc chắn lý do là nó làm tăng tính song song của cấp độ hướng dẫn, như bạn đề xuất.
dsimcha

2
Câu trả lời tốt đẹp và ví dụ hướng dẫn. Mặc dù tôi không thấy sự cố về bộ nhớ cache có thể ảnh hưởng đến hiệu suất như thế nào đối với ví dụ cụ thể này . Tôi tự giải thích cho mình sự khác biệt về hiệu suất giữa hai đoạn mã (trên máy của tôi, đoạn mã thứ hai nhanh hơn 2-3 lần) bằng cách lưu ý rằng đoạn mã đầu tiên vô hiệu hóa bất kỳ loại song song cấp lệnh nào trong các làn dấu chấm động. Phương thức thứ hai sẽ cho phép một CPU siêu vô hướng thực thi tối đa bốn phép cộng dấu phẩy động cùng một lúc.
Toby Brull

2
Hãy nhớ rằng kết quả sẽ không giống về số với vòng lặp ban đầu khi tính tổng theo cách này.
Barabas

Sự phụ thuộc được thực hiện trong vòng lặp là một chu kỳ , là phép cộng. Một lõi OoO sẽ hoạt động tốt. Ở đây, việc hủy cuộn có thể giúp ích cho SIMD dấu phẩy động, nhưng đó không phải là về OoO.
Veedrac

2
@Nils: Không nhiều lắm; CPU x86 OoO chủ đạo vẫn đủ tương tự như Core2 / Nehalem / K10. Bắt kịp sau khi bỏ lỡ bộ nhớ cache vẫn còn khá nhỏ, ẩn độ trễ FP vẫn là lợi ích chính. Vào năm 2010, các CPU có thể thực hiện 2 lần tải mỗi xung nhịp thậm chí còn hiếm hơn (chỉ AMD vì SnB chưa được phát hành), vì vậy nhiều bộ tích lũy chắc chắn ít có giá trị hơn đối với mã số nguyên so với bây giờ (tất nhiên đây là mã vô hướng nên tự động vectơ hóa , vì vậy ai mà biết được liệu trình biên dịch sẽ chuyển nhiều ắc vào yếu tố vector hoặc thành nhiều vector ắc ...)
Peter Cordes

25

Những điều đó sẽ không tạo ra sự khác biệt nào vì bạn đang thực hiện cùng một số phép so sánh. Đây là một ví dụ tốt hơn. Thay vì:

for (int i=0; i<200; i++) {
  doStuff();
}

viết:

for (int i=0; i<50; i++) {
  doStuff();
  doStuff();
  doStuff();
  doStuff();
}

Ngay cả khi đó gần như chắc chắn sẽ không thành vấn đề nhưng bây giờ bạn đang thực hiện 50 phép so sánh thay vì 200 (hãy tưởng tượng phép so sánh phức tạp hơn).

Hướng dẫn sử dụng vòng lặp unrolling nói chung phần lớn là một artifact của lịch sử tuy nhiên. Đó là một danh sách khác ngày càng tăng của những thứ mà một trình biên dịch tốt sẽ làm cho bạn khi nó quan trọng. Ví dụ, hầu hết mọi người không thèm viết x <<= 1hoặc x += xthay vào đó x *= 2. Bạn chỉ cần viết x *= 2và trình biên dịch sẽ tối ưu hóa nó cho bạn theo bất kỳ điều gì tốt nhất.

Về cơ bản, ngày càng ít cần phải đoán trước trình biên dịch của bạn.


1
@Mike Chắc chắn tắt tối ưu hóa nếu một ý tưởng hay khi phân vân, nhưng nó đáng để đọc liên kết mà Poita_ đã đăng. Các trình biên dịch đang rất giỏi trong công việc kinh doanh đó.
dmckee --- ex-moderator kitten.

16
@Mike "Tôi hoàn toàn có khả năng quyết định khi nào hoặc không nên làm những điều đó" ... Tôi nghi ngờ điều đó, trừ khi bạn là siêu nhân.
Mr. Boy,

5
@John: Tôi không biết tại sao bạn lại nói như vậy; mọi người dường như nghĩ rằng tối ưu hóa là một loại nghệ thuật đen mà chỉ những người biên dịch và những người đoán giỏi mới biết cách làm. Tất cả đều phụ thuộc vào hướng dẫn và chu kỳ và lý do tại sao chúng được sử dụng. Như tôi đã giải thích nhiều lần về SO, thật dễ dàng để biết làm thế nào và tại sao những khoản đó được chi tiêu. Nếu tôi có một vòng lặp phải sử dụng một phần trăm thời gian đáng kể và nó dành quá nhiều chu kỳ trong chi phí vòng lặp, so với nội dung, tôi có thể thấy điều đó và bỏ cuộn nó. Tương tự cho việc nâng mã. Nó không cần một thiên tài.
Mike Dunlavey

3
Tôi chắc rằng nó không quá khó, nhưng tôi vẫn nghi ngờ bạn có thể làm điều đó nhanh như trình biên dịch. Vấn đề với trình biên dịch làm điều đó cho bạn là gì? Nếu bạn không thích nó, chỉ cần tắt tối ưu hóa và đốt cháy thời gian của bạn như năm 1990!
Mr. Boy,

2
Hiệu suất đạt được do bỏ cuộn vòng lặp không liên quan gì đến các so sánh mà bạn đang lưu. Không có gì đâu.
bobbogo

14

Bất kể dự đoán rẽ nhánh trên phần cứng hiện đại, hầu hết các trình biên dịch đều thực hiện giải nén vòng lặp cho bạn.

Sẽ rất đáng giá khi tìm ra mức độ tối ưu hóa mà trình biên dịch của bạn mang lại cho bạn.

Tôi thấy bài thuyết trình của Felix von Leitner rất sáng tạo về chủ đề này. Tôi khuyên bạn nên đọc nó. Tóm tắt: Các trình biên dịch hiện đại RẤT thông minh, vì vậy việc tối ưu hóa bằng tay hầu như không bao giờ hiệu quả.


7
Đó là một bài đọc tốt, nhưng phần duy nhất tôi nghĩ là đáng chú ý là nơi anh ấy nói về việc giữ cho cấu trúc dữ liệu đơn giản. Phần còn lại của nó là chính xác, nhưng dựa trên một giả định unstated khổng lồ - đó là những gì đang được thực hiện được. Trong quá trình điều chỉnh mà tôi thực hiện, tôi thấy mọi người lo lắng về việc bỏ lỡ thanh ghi và bộ nhớ cache khi lượng thời gian khổng lồ đang trôi vào hàng núi mã trừu tượng không cần thiết.
Mike Dunlavey,

4
"tối ưu hóa tay hầu như không bao giờ hiệu quả" → Có lẽ đúng nếu bạn là người hoàn toàn mới với nhiệm vụ. Đơn giản là không đúng nếu không.
Veedrac

Vào năm 2019, tôi vẫn thực hiện các thao tác giải nén thủ công với lợi nhuận đáng kể so với các nỗ lực tự động của trình biên dịch .. vì vậy không đáng tin cậy lắm khi để trình biên dịch làm tất cả. Có vẻ như nó không thường xuyên gỡ bỏ tất cả. Ít nhất đối với c # tôi không thể nói thay mặt tất cả các ngôn ngữ.
WDUK

2

Theo như tôi hiểu, các trình biên dịch hiện đại đã mở các vòng lặp khi thích hợp - một ví dụ là gcc, nếu được thông qua các cờ tối ưu hóa, hướng dẫn sử dụng cho biết nó sẽ:

Bỏ cuộn các vòng mà số lần lặp có thể được xác định tại thời điểm biên dịch hoặc khi vào vòng lặp.

Vì vậy, trong thực tế, có khả năng trình biên dịch của bạn sẽ thực hiện những trường hợp nhỏ nhặt cho bạn. Do đó, tùy thuộc vào bạn để đảm bảo rằng càng nhiều vòng lặp càng tốt để trình biên dịch dễ dàng xác định số lần lặp sẽ cần thiết.


Các trình biên dịch đúng lúc thường không thực hiện thao tác giải nén vòng lặp, tính toán kinh nghiệm quá đắt. Trình biên dịch tĩnh có thể dành nhiều thời gian hơn cho nó, nhưng sự khác biệt giữa hai cách thống trị là rất quan trọng.
Abel

2

Giải nén vòng lặp, cho dù đó là giải phóng bằng tay hay giải phóng trình biên dịch, thường có thể phản tác dụng, đặc biệt là với các CPU x86 mới hơn (Core 2, Core i7). Điểm mấu chốt: điểm chuẩn cho mã của bạn có và không có vòng lặp mở trên bất kỳ CPU nào bạn định triển khai mã này.


Tại sao lại đặc biệt là trên các CPU x86?
JohnTortugo

7
@JohnTortugo: Các CPU x86 hiện đại có một số tối ưu nhất định đối với các vòng lặp nhỏ - xem ví dụ: Loop Stream Detector trên Core và Nehalem achitectures - việc giải nén một vòng lặp để nó không còn đủ nhỏ để vừa với bộ nhớ đệm LSD sẽ đánh bại sự tối ưu hóa này. Xem ví dụ: tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html
Paul R

1

Cố gắng mà không biết không phải là cách để làm.
Việc sắp xếp này có chiếm tỷ lệ cao trong tổng thời gian không?

Tất cả những gì mà việc mở vòng lặp làm là giảm chi phí vòng lặp tăng / giảm, so sánh với điều kiện dừng và nhảy. Nếu những gì bạn đang làm trong vòng lặp cần nhiều chu kỳ hướng dẫn hơn chính chi phí của vòng lặp, bạn sẽ không thấy nhiều cải thiện về phần trăm khôn ngoan.

Đây là một ví dụ về cách đạt được hiệu suất tối đa.


1

Hủy cuộn vòng lặp có thể hữu ích trong các trường hợp cụ thể. Lợi ích duy nhất không phải là bỏ qua một số bài kiểm tra!

Ví dụ, nó có thể cho phép thay thế vô hướng, chèn hiệu quả tìm nạp trước phần mềm ... Bạn sẽ ngạc nhiên thực sự nó có thể hữu ích như thế nào (bạn có thể dễ dàng tăng tốc 10% trên hầu hết các vòng lặp ngay cả với-3) bằng cách nhanh chóng mở cuộn.

Như đã nói trước đây, nó phụ thuộc rất nhiều vào vòng lặp và trình biên dịch và thử nghiệm là cần thiết. Thật khó để đưa ra quy tắc (hoặc trình biên dịch heuristic để giải nén sẽ hoàn hảo)


0

Việc hủy cuộn vòng hoàn toàn phụ thuộc vào kích thước vấn đề của bạn. Nó hoàn toàn phụ thuộc vào thuật toán của bạn có thể giảm kích thước thành các nhóm công việc nhỏ hơn. Những gì bạn đã làm ở trên không giống như vậy. Tôi không chắc liệu mô phỏng monte carlo thậm chí có thể được cuộn hay không.

Tôi kịch bản tốt cho việc mở vòng lặp sẽ xoay một hình ảnh. Vì bạn có thể luân chuyển các nhóm công việc riêng biệt. Để làm cho điều này hoạt động, bạn sẽ phải giảm số lần lặp lại.


Tôi đang mở một loại nhanh được gọi từ vòng lặp bên trong của mô phỏng của tôi, không phải vòng lặp chính của mô phỏng.
dsimcha

0

Bỏ cuộn vòng lặp vẫn hữu ích nếu có nhiều biến cục bộ cả trong và với vòng lặp. Để sử dụng lại những thanh ghi đó nhiều hơn thay vì lưu một thanh ghi cho chỉ mục vòng lặp.

Trong ví dụ của bạn, bạn sử dụng một lượng nhỏ các biến cục bộ, không lạm dụng các thanh ghi.

So sánh (đến kết thúc vòng lặp) cũng là một nhược điểm lớn nếu so sánh nặng (tức là không có testhướng dẫn), đặc biệt nếu nó phụ thuộc vào một chức năng bên ngoài.

Việc giải nén vòng lặp cũng giúp nâng cao nhận thức của CPU đối với dự đoán nhánh, nhưng những điều đó vẫn xảy ra.

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.