mentre (1) vs. per (;;) C’è una differenza di velocità?

Versione lunga …

Un collega ha affermato oggi dopo aver visto il mio uso di while (1) in uno script Perl che for (;;) è più veloce. Ho sostenuto che dovevano essere uguali sperando che l’interprete ottimizzasse le differenze. Ho creato uno script che avrebbe eseguito 1.000.000.000 per iterazioni di loop e lo stesso numero di cicli while e registrato l’intervallo di tempo. Non ho trovato alcuna differenza apprezzabile. Il mio collega ha detto che un professore gli aveva detto che nel while (1) stava facendo un confronto 1 == 1 e il for (;;) non lo era. Abbiamo ripetuto lo stesso test con il 100x il numero di iterazioni con C ++ e la differenza era trascurabile. Era comunque un esempio grafico di quanto codice compilato molto più veloce può essere contro un linguaggio di scripting.

Versione breve…

C’è qualche motivo per preferire un while (1) a un for (;;) se hai bisogno di un ciclo infinito da cui uscire?

Nota: se non è chiaro dalla domanda. Questa è stata una semplice discussione accademica tra una coppia di amici. Sono consapevole che questo non è un concetto super importante su cui tutti i programmatori dovrebbero agonizzare. Grazie per tutte le grandi risposte che ho ricevuto (e sono sicuro che gli altri) hanno imparato alcune cose da questa discussione.

Aggiornamento: il collega di cui sopra pesa con una risposta di seguito.

Citato qui nel caso in cui viene sepolto.

Veniva da un programmatore di assemblaggio AMD. Ha affermato che i programmatori C (i poeple) non si rendono conto che il loro codice ha inefficienze. Oggi ha detto che i compilatori di gcc sono molto bravi e mettono le persone come lui fuori dal mercato. Ha detto per esempio, e mi ha detto del while 1 vs for(;;) . Lo uso ora per abitudine ma gcc e soprattutto gli interpreti faranno la stessa operazione (un salto del processore) per entrambi questi giorni, dal momento che sono ottimizzati.

In perl, risultano negli stessi opcode:

 $ perl -MO=Concise -e 'for(;;) { print "foo\n" }' a < @> leave[1 ref] vKP/REFC ->(end) 1 <0> enter ->2 2 < ;> nextstate(main 2 -e:1) v ->3 9 <2> leaveloop vK/2 ->a 3 < {> enterloop(next->8 last->9 redo->4) v ->4 - < @> lineseq vK ->9 4 < ;> nextstate(main 1 -e:1) v ->5 7 < @> print vK ->8 5 <0> pushmark s ->6 6 < $> const[PV "foo\n"] s ->7 8 <0> unstack v ->4 -e syntax OK $ perl -MO=Concise -e 'while(1) { print "foo\n" }' a < @> leave[1 ref] vKP/REFC ->(end) 1 <0> enter ->2 2 < ;> nextstate(main 2 -e:1) v ->3 9 <2> leaveloop vK/2 ->a 3 < {> enterloop(next->8 last->9 redo->4) v ->4 - < @> lineseq vK ->9 4 < ;> nextstate(main 1 -e:1) v ->5 7 < @> print vK ->8 5 <0> pushmark s ->6 6 < $> const[PV "foo\n"] s ->7 8 <0> unstack v ->4 -e syntax OK 

Allo stesso modo in GCC:

 #include  void t_while() { while(1) printf("foo\n"); } void t_for() { for(;;) printf("foo\n"); } .file "test.c" .section .rodata .LC0: .string "foo" .text .globl t_while .type t_while, @function t_while: .LFB2: pushq %rbp .LCFI0: movq %rsp, %rbp .LCFI1: .L2: movl $.LC0, %edi call puts jmp .L2 .LFE2: .size t_while, .-t_while .globl t_for .type t_for, @function t_for: .LFB3: pushq %rbp .LCFI2: movq %rsp, %rbp .LCFI3: .L5: movl $.LC0, %edi call puts jmp .L5 .LFE3: .size t_for, .-t_for .section .eh_frame,"a",@progbits .Lframe1: .long .LECIE1-.LSCIE1 .LSCIE1: .long 0x0 .byte 0x1 .string "zR" .uleb128 0x1 .sleb128 -8 .byte 0x10 .uleb128 0x1 .byte 0x3 .byte 0xc .uleb128 0x7 .uleb128 0x8 .byte 0x90 .uleb128 0x1 .align 8 .LECIE1: .LSFDE1: .long .LEFDE1-.LASFDE1 .LASFDE1: .long .LASFDE1-.Lframe1 .long .LFB2 .long .LFE2-.LFB2 .uleb128 0x0 .byte 0x4 .long .LCFI0-.LFB2 .byte 0xe .uleb128 0x10 .byte 0x86 .uleb128 0x2 .byte 0x4 .long .LCFI1-.LCFI0 .byte 0xd .uleb128 0x6 .align 8 .LEFDE1: .LSFDE3: .long .LEFDE3-.LASFDE3 .LASFDE3: .long .LASFDE3-.Lframe1 .long .LFB3 .long .LFE3-.LFB3 .uleb128 0x0 .byte 0x4 .long .LCFI2-.LFB3 .byte 0xe .uleb128 0x10 .byte 0x86 .uleb128 0x2 .byte 0x4 .long .LCFI3-.LCFI2 .byte 0xd .uleb128 0x6 .align 8 .LEFDE3: .ident "GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3" .section .note.GNU-stack,"",@progbits 

Quindi immagino che la risposta sia, sono gli stessi in molti compilatori. Naturalmente, per alcuni altri compilatori questo potrebbe non essere necessariamente il caso, ma è probabile che il codice all’interno del ciclo sarà comunque qualche migliaio di volte più costoso del loop stesso, quindi a chi importa?

Usando GCC, entrambi sembrano compilare lo stesso linguaggio assembly:

 L2: jmp L2 

Non c’è molto motivo per preferire l’uno sull’altro. Penso che while(1) e in particolare while(true) sono più leggibili rispetto for(;;) , ma questa è solo la mia preferenza.

Non c’è differenza secondo lo standard. 6.5.3 / 1 ha:

La dichiarazione per

 for ( for-init-statement ; conditionopt ; expressionopt ) statement 

è equivalente a

 { for-init-statement while ( condition ) { statement expression ; } } 

E 6.5.3 / 2 ha:

È ansible omettere una o entrambe le condizioni e l’espressione. Una condizione mancante rende la clausola while implicita mentre while (true).

Quindi, secondo lo standard C ++, il codice:

 for (;;); 

è esattamente la stessa di:

 { while (true) { ; ; } } 

Il compilatore Visual C ++ utilizzato per emettere un avviso per

 while (1) 

(espressione costante) ma non per

 for (;;) 

Ho continuato la pratica di preferire for (;;) per questo motivo, ma non so se il compilatore lo fa ancora oggigiorno.

for(;;) è un carattere in meno da digitare se si vuole andare in quella direzione per ottimizzare le cose.

Turbo C con questo vecchio compilatore for(;;) produce un codice più veloce quindi while(1) .

Oggi gcc, i compilatori di Visual C (penso quasi tutti) si ottimizzano bene e le CPU con 4,7 MHz vengono utilizzate raramente.

In quei giorni a for( i=10; i; i-- ) era più veloce di for( i=1; i < =10; i++ ) , perché confrontare i è 0, si traduce in un salto condizionato con CPU-Zero-Flag. E lo Zero-Flag è stato modificato con l'ultima operazione di decremento ( i-- ) , non è necessaria alcuna operazione in cmp extra.

  call __printf_chk decl %ebx %ebx=iterator i jnz .L2 movl -4(%ebp), %ebx leave 

e qui con for(i=1; i< =10; i++) con cmpl extra:

  call __printf_chk incl %ebx cmpl $11, %ebx jne .L2 movl -4(%ebp), %ebx leave 

Per tutte le persone che discutono non si dovrebbe usare indefinito durante loops, e suggerendo cose daft come usare open goto ‘s (seriamente, ahi)

 while (1) { last if( condition1 ); code(); more_code(); last if( condition2 ); even_more_code(); } 

Non si può davvero rappresentare efficacemente in altro modo. Non senza creare una variabile di uscita e fare la magia nera per mantenerla sincronizzata.

Se hai un debole per la syntax più goto-esque, usa qualcosa di sano che limiti l’ambito.

 flow: { if ( condition ){ redo flow; } if ( othercondition ){ redo flow; } if ( earlyexit ){ last flow; } something(); # doesn't execute when earlyexit is true } 

In definitiva, la velocità non è così importante

Preoccuparsi di quanto siano efficaci i diversi costrutti di looping della velocità è un enorme spreco di tempo. Ottimizzazione prematura attraverso e attraverso. Non riesco a pensare a nessuna situazione che abbia mai visto in cui il codice di profilazione abbia trovato colli di bottiglia nella scelta del costrutto di looping.

Generalmente è come il ciclo e il ciclo.

Dovresti “ottimizzare” per la leggibilità e la concisione, e scrivere qualsiasi cosa sia meglio per spiegare il problema al prossimo povero sucker che trova il tuo codice.

Se usi il trucco “goto LABEL” che qualcuno ha menzionato, e devo usare il tuo codice, preparati a dormire con un occhio aperto, specialmente se lo fai più di una volta, perché quel genere di cose crea orribilmente il codice degli spaghetti.

Solo perché puoi creare codice spaghetti non significa che dovresti

Da Stroustrup, TC ++ PL (3a edizione), §6.1.1:

La curiosa notazione for (;;) è il modo standard per specificare un ciclo infinito; potresti pronunciarlo “per sempre”. […] while (true) è un’alternativa.

Preferisco for (;;) .

Ne ho sentito parlare una volta.

Veniva da un programmatore di assemblaggio AMD. Ha affermato che i programmatori C (le persone) non si rendono conto che il loro codice ha inefficienze. Oggi ha detto che i compilatori di gcc sono molto bravi e mettono le persone come lui fuori dal mercato. Ha detto per esempio, e mi ha detto del while 1 vs for(;;) . Lo uso ora per abitudine ma gcc e soprattutto gli interpreti faranno la stessa operazione (un salto del processore) per entrambi questi giorni, dal momento che sono ottimizzati.

Se il compilatore non esegue alcuna ottimizzazione, for(;;) sarebbe sempre più veloce di while(true) . Questo perché while-statement valuta ogni volta la condizione, ma la for-statement è un salto incondizionato. Ma se il compilatore ottimizza il stream di controllo, potrebbe generare alcuni codici opzionali. Puoi leggere il codice di sassembly molto facilmente.

PS potresti scrivere un ciclo infinito come questo:

 #define EVER ;; //... for (EVER) { //... } 

In una build ottimizzata di un linguaggio compilato, non dovrebbe esserci differenza apprezzabile tra i due. Nessuno dei due dovrebbe terminare alcun confronto in fase di esecuzione, essi eseguiranno semplicemente il codice di loop fino a quando non si uscirà manualmente dal ciclo (ad es. Con una break ).

Sono sorpreso dal fatto che nessuno sia stato testato correttamente for (;;) versus while (1) in perl!

Poiché perl è un linguaggio interpretato, il tempo di eseguire uno script perl non consiste solo nella fase di esecuzione (che in questo caso è la stessa) ma anche nella fase di interpretazione prima dell’esecuzione. Entrambe queste fasi devono essere prese in considerazione quando si effettua un confronto di velocità.

Fortunatamente perl ha un comodo modulo Benchmark che possiamo usare per implementare un benchmark come segue:

 #!/usr/bin/perl -w use Benchmark qw( cmpthese ); sub t_for { eval 'die; for (;;) { }'; } sub t_for2 { eval 'die; for (;;) { }'; } sub t_while { eval 'die; while (1) { }'; } cmpthese(-60, { for => \&t_for, for2 => \&t_for2, while => \&t_while }); 

Si noti che sto testando due diverse versioni del ciclo infinito per: uno che è più corto del ciclo while e un altro che ha uno spazio extra per renderlo della stessa lunghezza del ciclo while.

Su Ubuntu 11.04 x86_64 con perl 5.10.1 ottengo i seguenti risultati:

           Vota per for2 while
 per 100588 / s - -0% -2%
 for2 100937 / s 0% - -1%
 mentre 102147 / s 2% 1% -

Il ciclo while è chiaramente il vincitore su questa piattaforma.

Su FreeBSD 8.2 x86_64 con perl 5.14.1:

          Vota per for2 while
 per 53453 / s - -0% -2%
 for2 53552 / s 0% - -2%
 mentre 54564 / s 2% 2% -

Anche se loop è il vincitore anche qui.

Su FreeBSD 8.2 i386 con perl 5.14.1:

          Vota mentre per for2
 mentre 24311 / s - -1% -1%
 per 24481 / s 1% - -1%
 for2 24637 / s 1% 1% -

Sorprendentemente il ciclo for con uno spazio extra è la scelta più veloce qui!

La mia conclusione è che il ciclo while dovrebbe essere usato sulla piattaforma x86_64 se il programmatore sta ottimizzando la velocità. Ovviamente si dovrebbe usare un ciclo for quando si ottimizza lo spazio. I miei risultati sono purtroppo inconcludenti per quanto riguarda altre piattaforms.

In teoria, un compilatore completamente ingenuo potrebbe memorizzare il letterale “1” nel binario (spazio inutile) e verificare se 1 == 0 ogni iterazione (sprecare tempo e più spazio).

In realtà, tuttavia, anche con le ottimizzazioni “no”, i compilatori continueranno a ridurre entrambi allo stesso modo. Possono anche emettere avvisi perché potrebbero indicare un errore logico. Ad esempio, l’argomento di while potrebbe essere definito da qualche altra parte e non ti rendi conto che è costante.

Sono sorpreso che nessuno abbia offerto la forma più diretta, corrispondente all’assemblea desiderata:

 forever: do stuff; goto forever; 

while(1) è un idioma per for(;;) che è riconosciuto dalla maggior parte dei compilatori.

Sono stato contento di vedere che perl riconosce until(0) , anche.

Per riassumere il dibattito for (;;) vs while (1) è ovvio che il primo era più veloce ai tempi dei vecchi compilatori non ottimizzanti, ecco perché si tende a vederlo in basi di codice più vecchie come il codice sorgente Unix Lions Il commento, tuttavia, nell’era dei coglioni che ottimizzano i compilatori, quei guadagni sono ottimizzati, accoppiando il fatto che con il fatto che quest’ultimo è più facile da capire rispetto al primo credo che sarebbe più preferibile.

Penserei che entrambi sono uguali in termini di prestazioni. Ma preferirei mentre (1) leggibilità, ma mi chiedo perché hai bisogno di un ciclo infinito.

Loro sono la stessa cosa. Ci sono domande molto più importanti su cui riflettere.