Czy Kompilatory wytwarzają lepszy kod dla pętli do-while w porównaniu z innymi typami pętli?
Jest komentarz w Zlib compression library (która jest używana m.in. w projekcie Chromium), który sugeruje, że pętla do-while W C generuje "lepszy" kod na większości kompilatorów. Oto fragment kodu, w którym się pojawia.
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
Czy są jakieś dowody na to, że większość (lub dowolne) Kompilatory generowałyby lepiej (np. bardziej wydajne) kod?
Aktualizacja: Mark Adler , jeden z oryginalnych autorów, podał trochę kontekstu w komentarzach.
6 answers
Po pierwsze:
A do-while
loop nie jest tym samym co while
-loop lub for
-loop.
-
Pętle
while
ifor
mogą w ogóle nie uruchamiać ciała pętli. - a
do-while
pętla zawsze uruchamia ciało pętli przynajmniej raz - pomija początkową kontrolę stanu.
while
lub for
nawet wtedy, gdy jest zagwarantowane, że zawsze będzie pętla przynajmniej raz. (Szczególnie w językach z pętlami foreach.)
Aby uniknąć porównywania jabłek i pomarańczy, kontynuuję zakładając, że pętla zawsze będzie działać przynajmniej raz. Co więcej, nie wspomnę więcej o pętlach for
, ponieważ są one zasadniczo pętlami while
z odrobiną cukru składniowego dla licznika pętli.
Więc odpowiem na pytanie:
jeśli pętla while
ma gwarancję pętli co najmniej raz, czy istnieje jakikolwiek wzrost wydajności dzięki użyciu pętli do-while
zamiast tego.
A do-while
pomija pierwszą kontrolę stanu. Więc jest jedna gałąź mniej i jeden warunek mniej do oceny.
Jeśli warunek jest drogi do sprawdzenia i wiesz, że masz gwarancję pętli co najmniej raz, wtedy pętla do-while
może być szybsza.
I chociaż jest to w najlepszym razie uważane za mikro-optymalizację, kompilator nie zawsze może to zrobić: w szczególności, gdy kompilator nie jest w stanie udowodnić, że pętla zawsze wejdzie co najmniej raz.
Innymi słowy, pętla while:
while (condition){
body
}
Jest faktycznie takie samo jak to:
if (condition){
do{
body
}while (condition);
}
Jeśli wiesz, że zawsze będziesz pętlą przynajmniej raz, to twierdzenie if-jest obce.
Podobnie na poziomie montażu, w przybliżeniu tak kompilują się różne pętle:
Pętla do-while:
start:
body
test
conditional jump to start
While-loop:
test
conditional jump to end
start:
body
test
conditional jump to start
end:
Zauważ, że warunek został zduplikowany. Alternatywne podejście jest:
unconditional jump to end
start:
body
end:
test
conditional jump to start
... który zamienia duplikat kodu na dodatkowy skok.
Tak czy siak, to i tak gorzej niż zwykła pętla do-while
.
To powiedziawszy, Kompilatory mogą robić, co chcą. A jeśli mogą udowodnić, że pętla zawsze wchodzi raz, to wykonała pracę za Ciebie.
Ale rzeczy są nieco dziwne dla konkretnego przykładu w pytaniu, ponieważ ma puste ciało pętli. Ponieważ nie ma ciała, nie ma logicznej różnicy między while
i do-while
.
FWIW, testowałem to w Visual Studio 2012:
Z pustym ciałem generuje ten sam kod dla
while
ido-while
. Więc ta część jest prawdopodobnie pozostałością po dawnych czasach, kiedy Kompilatory nie były tak wielkie.Ale z niepustym ciałem, VS2012 udaje się uniknąć powielania kodu warunkowego, ale nadal generuje dodatkowy skok warunkowy.
Więc to ironia, że podczas gdy przykład w pytaniu podkreśla, dlaczego pętla do-while
może być szybsza w ogólnym przypadku, sam przykład nie wydaje się dawać żadnych korzyści nowoczesnemu kompilatorowi.
Biorąc pod uwagę, jak stary był komentarz, możemy tylko zgadywać, dlaczego miałoby to mieć znaczenie. Jest bardzo możliwe, że Kompilatory w tym czasie nie były w stanie rozpoznać, że ciało jest puste. (A jeśli tak, to nie wykorzystali informacji.)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-27 08:01:14
Czy są jakieś dowody na to, że większość (lub dowolne) kompilatorów generowałaby lepszy (np. wydajniejszy) kod?
Niewiele, chyba że spojrzysz na rzeczywiste wygenerowane Zgromadzenie rzeczywistego, specyficznego kompilatora na konkretnej platformie z pewnymi specyficznymi ustawieniami optymalizacji.
To było prawdopodobnie warte martwienia się o dziesiątki lat temu( kiedy Zlib został napisany), ale na pewno nie teraz, chyba że znalazłeś, przez prawdziwe profilowanie, że usunie to wąskie gardło z twojego kodu.
W skrócie (tl; dr):
Interpretuję komentarz w kodzie OPs trochę inaczej, myślę ,że "lepszy kod", który twierdzą, że zaobserwowali, był spowodowany przeniesieniem rzeczywistej pracy do pętli "warunek". Całkowicie się jednak Zgadzam, że jest to bardzo specyficzne dla kompilatora i że porównanie, które zrobili, będąc w stanie wyprodukować nieco inny kod, jest w większości bezcelowe i prawdopodobnie przestarzałe, jak pokazuję poniżej.
Szczegóły:
Trudno powiedzieć, co oryginalny autor miał na myśli w swoim komentarzu na temat tego do {} while
tworzenia lepszego kodu, ale chciałbym spekulować w innym kierunku niż to, co zostało tutaj podniesione - uważamy, że różnica między do {} while
i while {}
pętlami jest dość szczupła (jedna gałąź mniej, jak powiedział mistyczny), ale jest w tym kodzie coś jeszcze "zabawniejszego", a to Wkładanie całej pracy do tego szalonego stanu i utrzymywanie wewnętrznej części pustej (do {}
).
Próbowałem następujących kod na gcc 4.8.1 (- O3) i daje ciekawą różnicę -
#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;
}
Po kompilacji -
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>
...
Więc pierwsza pętla wykonuje 7 instrukcji, podczas gdy druga wykonuje 6, mimo że mają wykonywać tę samą pracę. Nie wiem, czy kryje się za tym jakiś sprytny kompilator, prawdopodobnie nie i jest to tylko przypadek, ale nie sprawdzałem, jak współdziała z innymi opcjami kompilatora, których ten projekt może używać.
Na clang 3.3 (- O3) na drugiej ręcznie, obie pętle generują ten kod 5 instrukcji:
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>
Co po prostu pokazuje, że Kompilatory są zupełnie inne i rozwijają się w znacznie szybszym tempie, niż niektórzy programiści mogli przewidzieć kilka lat temu. Oznacza to również, że ten komentarz jest dość bezsensowny i prawdopodobnie tam, ponieważ nikt nigdy nie sprawdzał, czy nadal ma sens.
Podsumowując-jeśli chcesz zoptymalizować do jak najlepszego kodu (i wiesz jak powinien wyglądać), zrób to bezpośrednio w assembly i wyciąć " middle-man "(kompilator) z równania, ale wziąć pod uwagę, że nowsze kompilatory i nowsze HW mogą uczynić tę optymalizację przestarzałą. W większości przypadków o wiele lepiej jest pozwolić kompilatorowi wykonać ten poziom pracy za Ciebie i skupić się na optymalizacji dużych rzeczy.
Kolejny punkt, który powinien być wykonany-liczenie instrukcji (zakładając, że po tym był oryginalny kod OPs), w żadnym wypadku nie jest dobrym pomiarem wydajności kodu. Nie wszystkie instrukcje były tworzone równe, a niektóre z nich (proste ruchy reg-to-reg dla np.) są naprawdę tanie, ponieważ są zoptymalizowane przez procesor. Inna optymalizacja może faktycznie zaszkodzić wewnętrznym optymalizacjom procesora, więc ostatecznie liczy się tylko właściwa analiza porównawcza.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-03 09:01:40
Pętla while
jest często kompilowana jako pętla do-while
z początkową odgałęzieniem do warunku, tj.
bra $1 ; unconditional branch to the condition
$2:
; loop body
$1:
tst <condition> ; the condition
brt $2 ; branch if condition true
Natomiast kompilacja pętli do-while
jest taka sama bez rozgałęzienia początkowego. Widać z tego, że jest on z natury mniej wydajny przez koszt początkowej gałęzi, która jest jednak opłacana tylko raz. [Porównaj z naiwnym sposobem implementacji while,
, który wymaga zarówno gałęzi warunkowej, jak i gałęzi bezwarunkowej na iterację.]
while
w pętlę do-while
i odwrotnie.Robią różne rzeczy. I w tym przypadku kilka wywołań metod całkowicie zdominowałoby to, co kompilator zrobił z while
, Jak w stosunku do do-while.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-26 22:27:26
Uwaga Nie dotyczy wyboru instrukcji sterowania (do vs. while), lecz rozwinięcia pętli!!!
Jak widać, jest to funkcja porównywania łańcuchów (elementy łańcuchowe mają prawdopodobnie 2 bajty), która mogła być zapisana za pomocą jednego porównania, a nie Czterech w skrócie-i wyrażeniu.
Ta ostatnia implementacja jest z pewnością szybsza, ponieważ wykonuje pojedyncze sprawdzenie warunku końca łańcucha po każdych porównaniach czterech elementów, podczas gdy standardowe kodowanie wymagałoby jednej kontroli na porównanie. Inaczej mówiąc, 5 testów na 4 elementy vs. 8 testów na 4 elementy.
W każdym razie, będzie działać tylko wtedy, gdy długość łańcucha jest wielokrotnością czterech lub ma element sentinel (tak, że dwa łańcuchy są gwarantowane różnią się poza granicą strend
). Dość ryzykowne !
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-26 21:20:58
Ta dyskusja o efektywności while vs. do jest całkowicie bezcelowa w tym przypadku, ponieważ nie ma ciała.
while (Condition)
{
}
I
do
{
}
while (Condition);
Są absolutnie równoważne.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-27 07:27:50