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.

Author: Community, 2013-11-24

6 answers

Po pierwsze:

A do-while loop nie jest tym samym co while-loop lub for-loop.

  • Pętle while i for 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.
Więc to jest logiczna różnica. To powiedziawszy, nie wszyscy ściśle przestrzegają tego. Często używa się pętli 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 i do-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.)

 108
Author: Mysticial,
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.

 24
Author: ,
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-24 08:28:20

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.

 16
Author: Leeor,
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ę.]

[[9]}Powiedziawszy to, oni nie są porównywalne alternatywy. Trudno jest przekształcić pętlę 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.
 10
Author: user207421,
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 !

 7
Author: Yves Daoust,
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.

 0
Author: Yves Daoust,
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