Các trình biên dịch có tạo ra mã tốt hơn cho vòng lặp do-while so với các loại vòng lặp khác không?


89

Có một nhận xét trong thư viện nén zlib (được sử dụng trong dự án Chromium trong số nhiều dự án khác) ngụ ý rằng vòng lặp do-while trong C tạo ra mã "tốt hơn" trên hầu hết các trình biên dịch. Đây là đoạn mã nơi nó xuất hiện.

do {
} while (*(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         scan < strend);
/* The funny "do {}" generates better code on most compilers */

https://code.google.com/p/chromium/codesearch#chromium/src/third_party/zlib/deflate.c&l=1225

Có bằng chứng nào cho thấy hầu hết (hoặc bất kỳ) trình biên dịch nào sẽ tạo ra mã tốt hơn (ví dụ: hiệu quả hơn) không?

Cập nhật: Mark Adler , một trong những tác giả gốc, đã đưa ra một chút bối cảnh trong các nhận xét.


7
Nhân tiện, để làm rõ, đây không phải là một phần của Chromium. Như bạn có thể suy luận từ URL, đây là một dự án "của bên thứ 3" và nếu bạn nhìn kỹ hơn, bạn có thể nhận ra rằng mã này là từ ZLib, một thư viện nén đa năng được sử dụng rộng rãi.

1
The funny "do {}" generates better code--- tốt hơn cái gì? Buồn cười hơn khi () hay nhàm chán, bình thường làm gì {}?
n. 'đại từ' m.

@ H2CO3 cảm ơn bạn đã giải thích rõ, mình đã chỉnh sửa câu hỏi để cụ thể hơn về nguồn gốc.
Dennis

42
Nhận xét đó đã được viết cách đây hơn 18 năm trong thời đại của các trình biên dịch Borland và Sun C. Mọi liên quan đến trình biên dịch ngày nay sẽ hoàn toàn là ngẫu nhiên. Lưu ý rằng cách sử dụng cụ thể này do, trái ngược với chỉ a whilekhông tránh được một nhánh có điều kiện.
Mark Adler

Câu trả lời:


108

Đầu tiên:

Một do-whilevòng lặp là không giống như một while-loop hoặc một for-loop.

  • whileforcác vòng lặp có thể không chạy phần thân của vòng lặp.
  • Một do-whilevòng lặp luôn chạy thân vòng lặp ít nhất một lần - nó bỏ qua việc kiểm tra điều kiện ban đầu.

Vì vậy, đó là sự khác biệt hợp lý. Nói như vậy nhưng không phải ai cũng tuân thủ nghiêm ngặt điều này. Vòng lặp for whilehoặc forđược sử dụng khá phổ biến ngay cả khi nó được đảm bảo rằng nó sẽ luôn lặp lại ít nhất một lần. (Đặc biệt là trong các ngôn ngữ có vòng lặp foreach .)

Vì vậy, để tránh so sánh táo và cam, tôi sẽ tiếp tục giả sử rằng vòng lặp sẽ luôn chạy ít nhất một lần. Hơn nữa, tôi sẽ không đề cập forlại các vòng lặp vì chúng về cơ bản là whilecác vòng lặp với một chút đường cú pháp cho bộ đếm vòng lặp.

Vì vậy, tôi sẽ trả lời câu hỏi:

Nếu một whilevòng lặp được đảm bảo lặp lại ít nhất một lần, thì liệu có tăng hiệu suất nào khi sử dụng một do-whilevòng lặp thay thế không.


A do-whilebỏ qua kiểm tra điều kiện đầu tiên. Vì vậy, có một nhánh ít hơn và một điều kiện ít hơn để đánh giá.

Nếu điều kiện đắt để kiểm tra và bạn biết mình được đảm bảo lặp lại ít nhất một lần, thì một do-whilevòng lặp có thể nhanh hơn.

Và mặc dù đây được coi là một tối ưu hóa vi mô tốt nhất, nhưng nó là một điều mà trình biên dịch không phải lúc nào cũng làm được: Cụ thể là khi trình biên dịch không thể chứng minh rằng vòng lặp sẽ luôn nhập ít nhất một lần.


Nói cách khác, một vòng lặp while:

while (condition){
    body
}

Hiệu quả giống như sau:

if (condition){
    do{
        body
    }while (condition);
}

Nếu bạn biết rằng bạn sẽ luôn lặp lại ít nhất một lần, thì câu lệnh if đó không liên quan.


Tương tự như vậy ở cấp độ lắp ráp, đây là cách các vòng lặp khác nhau biên dịch thành:

vòng lặp do-while:

start:
    body
    test
    conditional jump to start

trong khi lặp lại:

    test
    conditional jump to end
start:
    body
    test
    conditional jump to start
end:

Lưu ý rằng điều kiện đã được nhân đôi. Một cách tiếp cận thay thế là:

    unconditional jump to end
start:
    body
end:
    test
    conditional jump to start

... loại bỏ mã trùng lặp để có thêm một bước nhảy.

Dù bằng cách nào, nó vẫn tệ hơn một do-whilevòng lặp bình thường .

Điều đó nói rằng, trình biên dịch có thể làm những gì họ muốn. Và nếu họ có thể chứng minh rằng vòng lặp luôn đi vào một lần, thì nó đã hoàn thành công việc cho bạn.


Nhưng mọi thứ hơi kỳ lạ đối với ví dụ cụ thể trong câu hỏi vì nó có phần thân vòng lặp trống. Vì không có cơ thể, không có sự khác biệt hợp lý giữa whiledo-while.

FWIW, tôi đã kiểm tra điều này trong Visual Studio 2012:

  • Với phần thân trống, nó thực sự tạo ra cùng một mã cho whiledo-while. Vì vậy, phần đó có thể là một phần còn lại của ngày xưa khi các trình biên dịch không tuyệt vời như vậy.

  • Nhưng với phần thân không trống, VS2012 quản lý để tránh trùng lặp mã điều kiện, nhưng vẫn tạo thêm một bước nhảy có điều kiện.

Vì vậy, thật mỉa mai rằng trong khi ví dụ trong câu hỏi nêu bật lý do tại sao một do-whilevòng lặp có thể nhanh hơn trong trường hợp chung, bản thân ví dụ này dường như không mang lại bất kỳ lợi ích nào trên một trình biên dịch hiện đại.

Xem xét mức độ cũ của nhận xét, chúng tôi chỉ có thể đoán tại sao nó lại quan trọng. Rất có thể các trình biên dịch vào thời điểm đó không có khả năng nhận ra rằng phần thân trống. (Hoặc nếu có, họ đã không sử dụng thông tin.)


12
Vì vậy, kiểm tra điều kiện một lần ít hơn một lợi thế lớn? Tôi rất nghi ngờ rằng. Chạy vòng lặp 100 lần và nó trở nên hoàn toàn không đáng kể.

7
@ H2CO3 Nhưng nếu vòng lặp chỉ chạy một hoặc hai lần? Và kích thước mã tăng lên từ mã điều kiện trùng lặp thì sao?
Mysticial

6
@Mystical Nếu một vòng lặp chỉ chạy một hoặc hai lần, thì vòng lặp đó không đáng để tối ưu hóa. Và kích thước mã tăng lên ... không phải là một đối số chắc chắn, tốt nhất. Nó không phải là một yêu cầu mà mọi trình biên dịch thực hiện nó theo cách bạn đã chỉ ra. Tôi đã viết một trình biên dịch cho ngôn ngữ đồ chơi của riêng mình và việc biên dịch các vòng lặp while được thực hiện với một bước nhảy vô điều kiện đến đầu vòng lặp, vì vậy mã cho điều kiện chỉ được phát ra một lần.

30
@ H2CO3 "Nếu một vòng lặp chỉ chạy một hoặc hai lần, thì vòng lặp đó không đáng để tối ưu hóa." - Tôi có ý kiến ​​khác. Nó có thể nằm trong một vòng lặp khác. Hàng tấn mã HPC được tối ưu hóa cao của riêng tôi là như thế này. Và vâng, do-while tạo ra sự khác biệt.
Mysticial

29
@ H2CO3 Tôi đã nói rằng tôi đang khuyến khích nó ở đâu? Câu hỏi hỏi là một do-whilevòng lặp nhanh hơn một vòng lặp while. Và tôi đã trả lời câu hỏi bằng cách nói rằng nó có thể nhanh hơn. Tôi không nói bao nhiêu. Tôi không nói liệu nó có đáng giá hay không. Tôi không khuyên bất kỳ ai bắt đầu chuyển đổi sang vòng lặp do-while. Nhưng chỉ đơn giản phủ nhận rằng có khả năng tối ưu hóa, ngay cả khi nó là một điều nhỏ, theo ý kiến ​​của tôi là một điều bất lợi đối với những người quan tâm và quan tâm đến những điều này.
Mysticial

24

Có bằng chứng nào cho thấy hầu hết (hoặc bất kỳ) trình biên dịch nào sẽ tạo ra mã tốt hơn (ví dụ: hiệu quả hơn) không?

Không nhiều, trừ khi bạn nhìn vào bản lắp ráp thực tế được tạo ra của một trình biên dịch cụ thể, thực tế trên một nền tảng cụ thể với một số cài đặt tối ưu hóa cụ thể.

Điều này có lẽ đáng lo ngại về nhiều thập kỷ trước (khi ZLib đã được viết), nhưng chắc chắn không phải là ngày nay, trừ khi bạn tìm thấy, bằng cách lập hồ sơ thực, điều này loại bỏ nút cổ chai khỏi mã của bạn.


9
Nói tốt - cụm từ premature optimizationxuất hiện trong tâm trí ở đây.
James Snell

@JamesSnell chính xác. Và đó là những gì câu trả lời được xếp hạng cao nhất ủng hộ / khuyến khích.

16
Tôi không nghĩ rằng câu trả lời được xếp hạng cao nhất khuyến khích tối ưu hóa sớm. Tôi lập luận rằng nó cho thấy sự khác biệt về hiệu quả là có thể xảy ra, tuy nhiên nó có thể nhỏ hoặc không đáng kể. Nhưng mọi người diễn giải mọi thứ theo cách khác và một số có thể coi đó là dấu hiệu để bắt đầu sử dụng vòng lặp do-while khi không cần thiết (tôi hy vọng là không). Dù sao, tôi rất vui với tất cả các câu trả lời cho đến nay. Họ cung cấp thông tin có giá trị cho câu hỏi và tạo ra cuộc thảo luận thú vị.
Dennis

16

Tóm lại (tl; dr):

Tôi đang giải thích nhận xét trong mã của OPs hơi khác một chút, tôi nghĩ rằng "mã tốt hơn" mà họ tuyên bố đã quan sát được là do chuyển công việc thực tế vào "điều kiện" của vòng lặp. Tuy nhiên, tôi hoàn toàn đồng ý rằng nó rất cụ thể cho trình biên dịch và so sánh mà họ đã thực hiện, mặc dù có thể tạo ra một mã hơi khác, nhưng hầu hết là vô nghĩa và có lẽ đã lỗi thời, như tôi trình bày bên dưới.


Chi tiết:

Thật khó để nói tác giả gốc có ý gì về nhận xét của anh ấy về việc do {} whiletạo ra mã tốt hơn này, nhưng tôi muốn suy đoán theo một hướng khác so với những gì được nêu ở đây - chúng tôi tin rằng sự khác biệt giữa do {} whilewhile {}các vòng lặp là khá mỏng (một nhánh ít hơn Mystical nói), nhưng có một cái gì đó thậm chí còn "hài hước hơn" trong đoạn mã này và điều đó đang đặt tất cả công việc vào bên trong tình trạng điên rồ này và giữ cho phần bên trong trống ( do {}).

Tôi đã thử đoạn mã sau trên gcc 4.8.1 (-O3) và nó mang lại sự khác biệt thú vị -

#include "stdio.h" 
int main (){
    char buf[10];
    char *str = "hello";
    char *src = str, *dst = buf;

    char res;
    do {                            // loop 1
        res = (*dst++ = *src++);
    } while (res);
    printf ("%s\n", buf);

    src = str;
    dst = buf;
    do {                            // loop 2
    } while (*dst++ = *src++);
    printf ("%s\n", buf);

    return 0; 
}

Sau khi biên dịch -

00000000004003f0 <main>:
  ... 
; loop 1  
  400400:       48 89 ce                mov    %rcx,%rsi
  400403:       48 83 c0 01             add    $0x1,%rax
  400407:       0f b6 50 ff             movzbl 0xffffffffffffffff(%rax),%edx
  40040b:       48 8d 4e 01             lea    0x1(%rsi),%rcx
  40040f:       84 d2                   test   %dl,%dl
  400411:       88 16                   mov    %dl,(%rsi)
  400413:       75 eb                   jne    400400 <main+0x10>
  ...
;loop 2
  400430:       48 83 c0 01             add    $0x1,%rax
  400434:       0f b6 48 ff             movzbl 0xffffffffffffffff(%rax),%ecx
  400438:       48 83 c2 01             add    $0x1,%rdx
  40043c:       84 c9                   test   %cl,%cl
  40043e:       88 4a ff                mov    %cl,0xffffffffffffffff(%rdx)
  400441:       75 ed                   jne    400430 <main+0x40>
  ...

Vì vậy, vòng lặp đầu tiên thực hiện 7 hướng dẫn trong khi vòng lặp thứ hai thực hiện 6, mặc dù chúng được cho là thực hiện cùng một công việc. Bây giờ, tôi thực sự không thể biết liệu có một số thông minh của trình biên dịch đằng sau điều này hay không, có lẽ không và nó chỉ là ngẫu nhiên nhưng tôi chưa kiểm tra cách nó tương tác với các tùy chọn trình biên dịch khác mà dự án này có thể đang sử dụng.


Mặt khác, trên clang 3.3 (-O3), cả hai vòng lặp đều tạo ra 5 mã lệnh này:

  400520:       8a 88 a0 06 40 00       mov    0x4006a0(%rax),%cl
  400526:       88 4c 04 10             mov    %cl,0x10(%rsp,%rax,1)
  40052a:       48 ff c0                inc    %rax
  40052d:       48 83 f8 05             cmp    $0x5,%rax
  400531:       75 ed                   jne    400520 <main+0x20>

Điều này chỉ cho thấy rằng các trình biên dịch khá khác biệt và tiến bộ với tốc độ nhanh hơn nhiều so với một số lập trình viên có thể đã dự đoán vài năm trước. Điều đó cũng có nghĩa là bình luận này khá vô nghĩa và có thể là ở đó bởi vì không ai đã từng kiểm tra xem nó có còn hợp lý hay không.


Điểm mấu chốt - nếu bạn muốn tối ưu hóa mã tốt nhất có thể (và bạn biết mã sẽ trông như thế nào), hãy thực hiện trực tiếp trong quá trình lắp ráp và cắt "người trung gian" (trình biên dịch) khỏi phương trình, nhưng hãy tính đến điều đó mới hơn trình biên dịch và HW mới hơn có thể làm cho việc tối ưu hóa này trở nên lỗi thời. Trong hầu hết các trường hợp, tốt hơn hết là chỉ để trình biên dịch thực hiện mức công việc đó cho bạn và tập trung vào việc tối ưu hóa những thứ lớn.

Một điểm khác cần được thực hiện - số lệnh (giả sử đây là mã của mã OP ban đầu), không có nghĩa là một phép đo tốt cho hiệu quả của mã. Không phải tất cả các hướng dẫn đều được tạo ra như nhau, và một số trong số chúng (ví dụ: chuyển đổi reg-to-reg đơn giản) thực sự rẻ vì chúng được CPU tối ưu hóa. Các tối ưu hóa khác thực sự có thể ảnh hưởng đến việc tối ưu hóa bên trong CPU, vì vậy cuối cùng chỉ tính điểm chuẩn thích hợp.


Có vẻ như nó tiết kiệm một lần di chuyển đăng ký. mov %rcx,%rsi:) Tôi có thể thấy cách sắp xếp lại mã có thể làm điều đó.
Mysticial

@Mystical, bạn đã đúng về tối ưu hóa vi mô. Đôi khi, ngay cả việc lưu một chỉ dẫn cũng không có giá trị gì (và các động thái reg-to-reg gần như miễn phí với việc đổi tên reg ngày nay).
Leeor

Có vẻ như động thái đổi tên không được thực hiện cho đến khi AMD Bulldozer và Intel Ivy Bridge. Đó là một điều bất ngờ!
Mysticial

@Mysticial, lưu ý rằng đây gần như là những bộ xử lý đầu tiên triển khai tệp đăng ký vật lý. Thiết kế cũ không theo thứ tự chỉ cần đặt thanh ghi vào bộ đệm sắp xếp lại, nơi bạn không thể làm điều đó.
Leeor

3
Có vẻ như bạn đã giải thích nhận xét trong mã gốc khác với hầu hết các mã và điều đó có ý nghĩa. Nhận xét có nội dung "hài hước làm {} .." nhưng không nói lên phiên bản không hài hước mà nó so sánh. Hầu hết mọi người đều biết sự khác biệt giữa do-while và while, vì vậy suy đoán của tôi là "điều buồn cười {}" không áp dụng cho điều đó, mà cho việc mở vòng lặp và / hoặc thiếu nhiệm vụ bổ sung, như bạn đã trình bày đây.
Abel

10

Một whilevòng lặp thường được biên dịch như một do-whilevòng lặp với một nhánh ban đầu cho điều kiện, tức là

    bra $1    ; unconditional branch to the condition
$2:
    ; loop body
$1:
    tst <condition> ; the condition
    brt $2    ; branch if condition true

trong khi việc biên dịch một do-whilevòng lặp giống nhau mà không có nhánh ban đầu. Bạn có thể thấy từ đó while()vốn đã kém hiệu quả hơn bởi chi phí của chi nhánh ban đầu, tuy nhiên chi nhánh này chỉ được trả một lần. [So sánh với cách triển khai ngây thơ while,yêu cầu cả nhánh có điều kiện và nhánh không điều kiện cho mỗi lần lặp.]

Phải nói rằng, chúng không thực sự là lựa chọn thay thế có thể so sánh được. Thật là đau đớn khi biến đổi một whilevòng lặp thành một do-whilevòng lặp và ngược lại.Họ làm những điều khác nhau. Và trong trường hợp này phương pháp một số cuộc gọi sẽ hoàn toàn thống trị bất cứ trình biên dịch đã làm với whilenhư chống lạido-while.


7

Nhận xét không phải là về sự lựa chọn của câu lệnh điều khiển (do so với while), nó là về việc mở vòng lặp !!!

Như bạn có thể thấy, đây là một hàm so sánh chuỗi (các phần tử chuỗi có thể dài 2 byte), có thể được viết bằng một phép so sánh duy nhất chứ không phải bốn trong một phím tắt và biểu thức.

Việc triển khai sau này chắc chắn nhanh hơn, vì nó thực hiện một lần kiểm tra điều kiện cuối chuỗi sau mỗi lần so sánh bốn phần tử, trong khi mã hóa tiêu chuẩn sẽ liên quan đến một lần kiểm tra cho mỗi lần so sánh. Nói cách khác, 5 kiểm tra trên 4 yếu tố so với 8 kiểm tra trên 4 yếu tố.

Dù sao, nó sẽ chỉ hoạt động nếu độ dài chuỗi là bội số của bốn hoặc có phần tử sentinel (để hai chuỗi được đảm bảo khác nhau qua strendđường viền). Khá rủi ro!


Đó là một quan sát thú vị và là điều mà mọi người đã bỏ qua cho đến bây giờ. Nhưng một trình biên dịch sẽ không có tác dụng gì đối với điều đó? Nói cách khác, nó sẽ luôn hiệu quả hơn bất kể trình biên dịch nào được sử dụng. Vậy tại sao lại có nhận xét đề cập đến trình biên dịch?
Dennis

@Dennis: các trình biên dịch khác nhau có các cách khác nhau để tối ưu hóa mã được tạo. Một số có thể tự mở vòng lặp (đối với một số mở rộng) hoặc tối ưu hóa các nhiệm vụ. Ở đây người viết mã buộc trình biên dịch vào vòng lặp-unrolling, làm cho các trình biên dịch ít tối ưu hóa vẫn hoạt động tốt. Tôi nghĩ Yves hoàn toàn đúng về giả định của mình, nhưng không có người viết mã ban đầu xung quanh, vẫn còn một chút bí ẩn về suy nghĩ thực sự đằng sau nhận xét "buồn cười".
Abel

1
@Abel cảm ơn bạn đã làm rõ, tôi hiểu ý nghĩa (giả định) đằng sau nhận xét tốt hơn bây giờ. Yves chắc chắn đã đến gần nhất để giải quyết bí ẩn đằng sau bình luận, nhưng tôi sẽ chấp nhận câu trả lời của Mysticial vì tôi nghĩ anh ấy đã trả lời câu hỏi của tôi tốt nhất. Hóa ra tôi đã đặt câu hỏi sai vì nhận xét đánh lừa tôi tập trung vào loại vòng lặp, trong khi nó có thể đề cập đến điều kiện.
Dennis

0

Trong trường hợp này, cuộc thảo luận về hiệu quả trong khi so với do là hoàn toàn vô nghĩa, vì không có cơ quan nào.

while (Condition)
{
}

do
{
}
while (Condition);

hoàn toàn tương đương.

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.