I compilatori producono un codice migliore per i cicli do-while rispetto ad altri tipi di cicli?


89

C'è un commento nella libreria di compressione zlib (che è usata nel progetto Chromium tra molti altri) che implica che un ciclo do-while in C genera codice "migliore" sulla maggior parte dei compilatori. Ecco lo snippet di codice in cui appare.

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'è qualche prova che la maggior parte (o qualsiasi) compilatori genererebbe codice migliore (ad esempio più efficiente)?

Aggiornamento: Mark Adler , uno degli autori originali, ha fornito un po 'di contesto nei commenti.


7
A proposito, per chiarire, questo non fa parte di Chromium. Come puoi dedurre dall'URL, questo è un progetto "di terze parti" e se lo guardi ancora più da vicino, puoi percepire che questo codice proviene da ZLib, una libreria di compressione generica e ampiamente utilizzata.

1
The funny "do {}" generates better code--- meglio di cosa? Che divertente mentre () o noioso, normale {}?
n. 'pronomi' m.

@ H2CO3 ti ringrazio per il chiarimento, ho modificato la domanda per essere più specifico sull'origine.
Dennis

42
Quel commento è stato scritto più di 18 anni fa nell'era dei compilatori Borland e Sun C. Qualsiasi rilevanza per i compilatori oggi sarebbe puramente accidentale. Si noti che questo particolare utilizzo di do, al contrario di solo a while, non evita un ramo condizionale.
Mark Adler

Risposte:


108

Prima di tutto:

Un do-whileciclo non è la stessa cosa di un whileciclo o di un forciclo.

  • whilee i forcicli potrebbero non eseguire affatto il corpo del ciclo.
  • Un do-whileciclo esegue sempre il corpo del ciclo almeno una volta: salta il controllo delle condizioni iniziali.

Quindi questa è la differenza logica. Detto questo, non tutti aderiscono strettamente a questo. È abbastanza comune utilizzare i loop whileo foranche quando è garantito che verrà sempre eseguito almeno una volta. (Soprattutto nelle lingue con cicli foreach .)

Quindi, per evitare di confrontare mele e arance, procederò supponendo che il ciclo verrà sempre eseguito almeno una volta. Inoltre, non menzionerò più i forloop poiché sono essenzialmente whileloop con un po 'di zucchero di sintassi per un contatore di loop.

Quindi risponderò alla domanda:

Se whileè garantito che un loop venga eseguito almeno una volta, c'è qualche miglioramento in termini di prestazioni dall'utilizzo di un do-whileloop.


A do-whilesalta il primo controllo delle condizioni. Quindi c'è un ramo in meno e una condizione in meno da valutare.

Se la condizione è costosa da controllare e sai di avere la certezza di eseguire il loop almeno una volta, un do-whileloop potrebbe essere più veloce.

E anche se nel migliore dei casi questa è considerata una microottimizzazione, è quella che il compilatore non può sempre fare: in particolare quando il compilatore non è in grado di dimostrare che il ciclo entrerà sempre almeno una volta.


In altre parole, un ciclo while:

while (condition){
    body
}

È effettivamente lo stesso di questo:

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

Se sai che ripeterai sempre almeno una volta, quell'istruzione if è estranea.


Allo stesso modo a livello di assembly, questo è più o meno il modo in cui i diversi loop si compilano per:

ciclo do-while:

start:
    body
    test
    conditional jump to start

while loop:

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

Notare che la condizione è stata duplicata. Un approccio alternativo è:

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

... che scambia il codice duplicato per un ulteriore salto.

Ad ogni modo, è ancora peggio di un normale do-whileloop.

Detto questo, i compilatori possono fare quello che vogliono. E se possono provare che il ciclo entra sempre una volta, allora ha fatto il lavoro per te.


Ma le cose sono un po 'strane per l'esempio particolare nella domanda perché ha un corpo del ciclo vuoto. Poiché non esiste un corpo, non esiste alcuna differenza logica tra whilee do-while.

FWIW, l'ho testato in Visual Studio 2012:

  • Con il corpo vuoto, genera effettivamente lo stesso codice per whilee do-while. Quindi quella parte è probabilmente un residuo dei vecchi tempi in cui i compilatori non erano così grandi.

  • Ma con un corpo non vuoto, VS2012 riesce a evitare la duplicazione del codice della condizione, ma genera comunque un salto condizionale aggiuntivo.

Quindi è ironico che mentre l'esempio nella domanda evidenzia perché un do-whileciclo potrebbe essere più veloce nel caso generale, l'esempio stesso non sembra dare alcun vantaggio su un compilatore moderno.

Considerando l'età del commento, possiamo solo immaginare perché sarebbe importante. È molto probabile che i compilatori all'epoca non fossero in grado di riconoscere che il corpo era vuoto. (O se lo hanno fatto, non hanno usato le informazioni.)


12
Quindi controllare le condizioni una volta in meno è un così grande vantaggio? Ne dubito fortemente. Esegui il ciclo 100 volte e diventa del tutto insignificante.

7
@ H2CO3 Ma cosa succede se il ciclo viene eseguito solo una o due volte? E che dire di quella dimensione del codice aumentata dal codice della condizione duplicato?
Mysticial

6
@Mystical Se un ciclo viene eseguito solo una o due volte, non vale la pena ottimizzarlo. E la maggiore dimensione del codice non è ... un argomento solido, nella migliore delle ipotesi. Non è un requisito che ogni compilatore lo implementa nel modo in cui hai mostrato. Ho scritto un compilatore per il mio linguaggio giocattolo e la compilazione dei cicli while è implementata con un salto incondizionato all'inizio del ciclo, quindi il codice per la condizione viene emesso una sola volta.

30
@ H2CO3 "Se un ciclo viene eseguito solo una o due volte, non vale la pena ottimizzarlo." - Mi permetto di dissentire. Potrebbe essere all'interno di un altro loop. Tonnellate del mio codice HPC altamente ottimizzato sono così. E sì, il do-while fa la differenza.
Mysticial

29
@ H2CO3 Dove ho detto che lo stavo incoraggiando? La domanda che si pone è un do-whileciclo più veloce di un ciclo while. E ho risposto alla domanda dicendo che può essere più veloce. Non ho detto di quanto. Non ho detto se ne valeva la pena. Non ho consigliato a nessuno di iniziare a convertire in cicli do-while. Ma negare semplicemente che ci sia la possibilità di un'ottimizzazione, anche se piccola, è a mio avviso un disservizio per chi ha a cuore e si interessa a queste cose.
Mysticial

24

C'è qualche prova che la maggior parte (o qualsiasi) compilatori genererebbe codice migliore (ad esempio più efficiente)?

Non molto, a meno che non si guardi l' effettivo assembly generato di un compilatore specifico e reale su una piattaforma specifica con alcune impostazioni di ottimizzazione specifiche.

Probabilmente valeva la pena preoccuparsi di questo circa decenni fa (quando ZLib è stato scritto), ma certamente non al giorno d'oggi, a meno che non si sia scoperto, mediante una profilazione reale, che ciò rimuove un collo di bottiglia dal codice.


9
Ben detto: la frase mi premature optimizationviene in mente qui.
James Snell

@JamesSnell esattamente. Ed è ciò che la risposta più votata supporta / incoraggia.

16
Non credo che la risposta più votata incoraggi un'ottimizzazione prematura. Direi che mostra che è possibile una differenza di efficienza, per quanto lieve o insignificante possa essere. Ma le persone interpretano le cose in modo diverso e alcuni potrebbero vederlo come un segno per iniziare a usare i cicli do-while quando non sono necessari (spero di no). Ad ogni modo, sono contento di tutte le risposte finora. Forniscono informazioni preziose rispetto alla domanda e hanno generato discussioni interessanti.
Dennis

16

In poche parole (tl; dr):

Sto interpretando il commento nel codice degli OP in modo leggermente diverso, penso che il "codice migliore" che affermano di aver osservato sia dovuto allo spostamento del lavoro effettivo nella "condizione" del ciclo. Sono tuttavia completamente d'accordo sul fatto che sia molto specifico per il compilatore e che il confronto che hanno fatto, pur essendo in grado di produrre un codice leggermente diverso, è per lo più inutile e probabilmente obsoleto, come mostro di seguito.


Dettagli:

È difficile dire cosa intendesse l'autore originale con il suo commento riguardo alla do {} whileproduzione di codice migliore, ma mi piacerebbe speculare in un'altra direzione rispetto a ciò che è stato sollevato qui: crediamo che la differenza tra i cicli do {} whilee while {}sia piuttosto sottile (un ramo in meno come Mystical ha detto), ma c'è qualcosa di ancora più "divertente" in questo codice e che sta mettendo tutto il lavoro all'interno di questa condizione folle, e mantenendo la parte interna vuota ( do {}).

Ho provato il seguente codice su gcc 4.8.1 (-O3), e dà una differenza interessante -

#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; 
}

Dopo la compilazione -

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>
  ...

Quindi il primo ciclo esegue 7 istruzioni mentre il secondo 6, anche se dovrebbero fare lo stesso lavoro. Ora, non posso davvero dire se c'è un po 'di intelligenza del compilatore dietro questo, probabilmente no ed è solo una coincidenza, ma non ho controllato come interagisce con altre opzioni del compilatore che questo progetto potrebbe utilizzare.


Su clang 3.3 (-O3) d'altra parte, entrambi i cicli generano questo codice di 5 istruzioni:

  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>

Il che sta solo a dimostrare che i compilatori sono abbastanza diversi e avanzano a un ritmo molto più veloce di quanto alcuni programmatori avrebbero potuto prevedere diversi anni fa. Significa anche che questo commento è piuttosto privo di significato e probabilmente c'è perché nessuno aveva mai verificato se avesse ancora senso.


Conclusione: se vuoi ottimizzare al miglior codice possibile (e sai come dovrebbe apparire), fallo direttamente in assembly e taglia il "middle-man" (compilatore) dall'equazione, ma tieni conto di quello più recente compilatori e HW più recenti potrebbero rendere obsoleta questa ottimizzazione. Nella maggior parte dei casi è molto meglio lasciare che il compilatore faccia quel livello di lavoro per te e concentrarsi sull'ottimizzazione delle cose importanti.

Un altro punto che dovrebbe essere fatto - il conteggio delle istruzioni (supponendo che questo sia ciò che stava cercando il codice dell'OP originale), non è affatto una buona misura per l'efficienza del codice. Non tutte le istruzioni sono state create uguali, e alcune di esse (semplici mosse reg-to-reg per esempio) sono davvero economiche in quanto vengono ottimizzate dalla CPU. Altre ottimizzazioni potrebbero effettivamente danneggiare le ottimizzazioni interne della CPU, quindi alla fine conta solo il benchmarking corretto.


Sembra che salvi una mossa del registro. mov %rcx,%rsi:) Posso vedere come riorganizzare il codice può farlo.
Mysticial

@Mystical, hai ragione sulla microottimizzazione però. A volte anche il salvataggio di una singola istruzione non vale nulla (e le mosse da registro a registro dovrebbero essere quasi gratuite con la ridenominazione di reg oggi).
Leeor

Non sembra che il cambio di nome sia stato implementato fino a AMD Bulldozer e Intel Ivy Bridge. È una sorpresa!
Mysticial

@Mysticial, nota che questi sono approssimativamente i primi processori che implementano un file di registro fisico. I vecchi progetti fuori ordine posizionano il registro nel buffer di riordino, dove non puoi farlo.
Leeor

3
Sembra che tu abbia interpretato il commento nel codice originale in modo diverso dalla maggior parte, e ha senso. Il commento dice "il divertente {} .." ma non dice a quale versione non divertente confronta. La maggior parte delle persone conosce la differenza tra do-while e while, quindi la mia ipotesi è che "il divertente {}" non si applicasse a questo, ma allo svolgimento del ciclo e / o alla mancanza del compito extra, come hai mostrato Qui.
Abel

10

Un whileciclo viene spesso compilato come un do-whileciclo con un ramo iniziale della condizione, ad es

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

mentre la compilazione di un do-whileciclo è la stessa senza il ramo iniziale. Si vede da ciò che while()è intrinsecamente meno efficiente dal costo del ramo iniziale, che però viene pagato una sola volta. [Confronta con il modo ingenuo di implementare while,che richiede sia un ramo condizionale che un ramo incondizionato per iterazione.]

Detto questo, non sono davvero alternative comparabili. È doloroso trasformare un whileloop in un do-whileloop e viceversa. Fanno cose diverse. E in questo caso le numerose chiamate al metodo domineranno totalmente qualunque cosa il compilatore abbia fatto whilecontrodo-while.


7

L'osservazione non riguarda la scelta dell'istruzione di controllo (do vs. while), riguarda lo svolgimento del ciclo !!!

Come puoi vedere, questa è una funzione di confronto tra stringhe (gli elementi stringa sono probabilmente lunghi 2 byte), che avrebbe potuto essere scritta con un singolo confronto anziché quattro in una scorciatoia ed espressione.

Quest'ultima implementazione è sicuramente più veloce, poiché esegue un singolo controllo della condizione di fine stringa dopo ogni confronto di quattro elementi, mentre la codifica standard comporterebbe un controllo per confronto. Detto diversamente, 5 test per 4 elementi vs 8 test per 4 elementi.

Ad ogni modo, funzionerà solo se la lunghezza della stringa è un multiplo di quattro o ha un elemento sentinella (in modo che sia garantito che le due stringhe differiscano oltre il strendbordo). Abbastanza rischioso!


Questa è un'osservazione interessante e qualcosa che tutti hanno trascurato fino ad ora. Ma un compilatore non avrebbe alcun effetto su questo? In altre parole, sarebbe sempre più efficiente indipendentemente dal compilatore utilizzato. Allora perché c'è un commento che menziona i compilatori?
Dennis

@Dennis: diversi compilatori hanno modi diversi di ottimizzare il codice generato. Alcuni possono eseguire il loop-srotolamento da soli (in qualche modo) o ottimizzare i compiti in trasferta. Qui il programmatore forza il compilatore nello srotolamento del ciclo, facendo in modo che i compilatori meno ottimizzati funzionino ancora bene. Penso che Yves abbia esattamente ragione sulle sue ipotesi, ma senza il programmatore originale in giro, rimane un po 'un mistero quale fosse il vero pensiero dietro l'osservazione "divertente".
Abel

1
@Abel grazie per aver chiarito, capisco meglio il significato (presunto) dietro il commento ora. Yves è sicuramente arrivato più vicino a risolvere il mistero dietro il commento, ma accetterò la risposta di Mysticial poiché penso che abbia risposto meglio alla mia domanda. Risulta che stavo facendo la domanda sbagliata perché il commento mi ha indotto in errore a concentrarmi sul tipo di loop, mentre probabilmente si riferisce alla condizione.
Dennis

0

Questa discussione sull'efficienza di while vs. do è completamente inutile in questo caso, poiché non esiste un corpo.

while (Condition)
{
}

e

do
{
}
while (Condition);

sono assolutamente equivalenti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.