Evita operatore Postfix Increment


25

Ho letto che dovrei evitare l'operatore di incremento postfix per motivi di prestazioni (in alcuni casi).

Ma ciò non influisce sulla leggibilità del codice? Secondo me:

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

Sembra migliore di:

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

Ma questo è probabilmente solo per abitudine. Certo, non ho visto molti usi ++i.

Le prestazioni sono così negative da sacrificare la leggibilità, in questo caso? O sono solo cieco ed ++iè più leggibile di i++?


1
L'ho usato i++prima di sapere che poteva influire sulle prestazioni ++i, quindi sono passato. All'inizio quest'ultimo sembrava un po 'strano, ma dopo un po' mi sono abituato e ora sembra naturale come i++.
gablin

15
++ie i++fare cose diverse in determinati contesti, non dare per scontato che siano uguali.
Orbling

2
Si tratta di C o C ++? Sono due lingue molto diverse! :-) In C ++ il for-loop idiomatico è for (type i = 0; i != 42; ++i). Non solo può operator++essere sovraccaricato, ma anche operator!=e operator<. L'incremento del prefisso non è più costoso di postfix, non uguale non è più costoso di minore di. Quali dovremmo usare?
Bo Persson,

7
Non dovrebbe essere chiamato ++ C?
Armand,

21
@Stephen: C ++ significa prendere C, aggiungerci e quindi usare quello vecchio .
supercat

Risposte:


58

I fatti:

  1. i ++ e ++ i sono ugualmente facili da leggere. Non ti piace perché non ci sei abituato, ma essenzialmente non c'è niente che tu possa interpretare erroneamente, quindi non c'è più lavoro da leggere o scrivere.

  2. In almeno alcuni casi, l'operatore postfix sarà meno efficiente.

  3. Tuttavia, nel 99,99% dei casi, non importa perché (a) agirà comunque su un tipo semplice o primitivo ed è solo un problema se sta copiando un grande oggetto (b) non sarà in una performance parte critica del codice (c) non sai se il compilatore lo ottimizzerà o meno, potrebbe farlo.

  4. Quindi, suggerisco di usare il prefisso, a meno che non sia specificamente necessario che Postfix sia una buona abitudine, solo perché (a) è una buona abitudine essere precisi con altre cose e (b) una volta in una luna blu si intende usare Postfix e capirlo nel modo sbagliato: se scrivi sempre quello che vuoi dire, è meno probabile. C'è sempre un compromesso tra prestazioni e ottimizzazione.

Dovresti usare il tuo buon senso e non micro-ottimizzare fino a quando non è necessario, ma non essere palesemente inefficiente per il gusto di farlo. In genere ciò significa: in primo luogo, escludere qualsiasi costruzione di codice inaccettabilmente inefficiente anche nel codice non critico in termini di tempo (normalmente qualcosa che rappresenta un errore concettuale fondamentale, come passare oggetti da 500 MB in valore senza motivo); e in secondo luogo, di ogni altro modo di scrivere il codice, scegliere il più chiaro.

Tuttavia, qui, credo che la risposta sia semplice: credo che scrivere il prefisso a meno che non sia specificamente necessario postfix sia (a) molto marginalmente più chiaro e (b) molto marginalmente più probabile che sia più efficiente, quindi dovresti sempre scriverlo per impostazione predefinita, ma non preoccuparti se lo dimentichi.

Sei mesi fa, pensavo lo stesso di te, che i ++ fosse più naturale, ma è puramente quello a cui sei abituato.

EDIT 1: Scott Meyers, in "Più efficace C ++" di cui mi fido generalmente su questa cosa, dice che dovresti in generale evitare di usare l'operatore postfix su tipi definiti dall'utente (perché l'unica implementazione sana della funzione di incremento postfix è quella di fare un copia dell'oggetto, chiamare la funzione di incremento del prefisso per eseguire l'incremento e restituire la copia, ma le operazioni di copia possono essere costose).

Quindi, non sappiamo se ci sono regole generali su (a) se ciò è vero oggi, (b) se si applica anche (meno) ai tipi intrinseci (c) se dovresti usare "++" su niente di più che una classe di iteratori leggeri di sempre. Ma per tutti i motivi che ho descritto sopra, non importa, fai quello che ho detto prima.

EDIT 2: si riferisce alla pratica generale. Se pensi che abbia importanza in qualche caso specifico, allora dovresti profilarlo e vedere. La profilazione è semplice ed economica e funziona. Dedurre dai primi principi ciò che deve essere ottimizzato è difficile e costoso e non funziona.


La tua pubblicazione è giusta per i soldi. Nelle espressioni in cui l'operatore infix + e post-incremento ++ sono stati sovraccaricati come aClassInst = someOtherClassInst + YetAnotherClassInst ++, il parser genererà il codice per eseguire l'operazione additiva prima di generare il codice per eseguire l'operazione post-incremento, alleviando la necessità di crea una copia temporanea. Il killer delle prestazioni qui non è post-incremento. È l'uso di un operatore infix sovraccarico. Gli operatori Infix producono nuove istanze.
bit-twiddler,

2
Sospetto fortemente che il motivo per cui le persone sono "abituate" i++piuttosto che a ++icausa del nome di un certo linguaggio di programmazione popolare a cui si fa riferimento in questa domanda / risposta ...
Shadow

61

Codificare sempre prima il programmatore e poi il computer.

Se c'è una differenza di prestazioni, dopo che il compilatore ha lanciato il suo occhio esperto sul tuo codice, E puoi misurarlo E importa - allora puoi cambiarlo.


7
Dichiarazione SUPERB !!!
Dave,

8
@Martin: ecco perché dovrei usare l'incremento del prefisso. La semantica di Postfix implica il mantenimento del vecchio valore e, se non ce n'è bisogno, non è corretto utilizzarlo.
Matthieu M.

1
Per un indice di loop che sarebbe più chiaro - ma se si stesse iterando su un array aumentando un puntatore e usando il prefisso significava partire da un indirizzo illegale uno prima dell'inizio che sarebbe stato negativo a prescindere da un aumento delle prestazioni
Martin Beckett

5
@Matthew: Semplicemente non è vero che il post-incremento implica mantenere una copia del vecchio valore. Non si può essere sicuri di come un compilatore gestisce i valori intermedi fino a quando non si visualizza il suo output. Se si prende il tempo per visualizzare il mio elenco di lingue di assembly generato da GCC annotato, vedrai che GCC genera lo stesso codice macchina per entrambi i loop. Questa assurdità nel favorire il pre-incremento rispetto al post-incremento perché più efficiente è poco più che una congettura.
bit-twiddler,

2
@Mathhieu: il codice che ho pubblicato è stato generato con l'ottimizzazione disattivata. La specifica C ++ non afferma che un compilatore deve produrre un'istanza temporanea di un valore quando viene utilizzata la post-incrementazione. Indica semplicemente la precedenza degli operatori pre e post-incremento.
bit-twiddler,

13

GCC produce lo stesso codice macchina per entrambi i loop.

Codice C.

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

Codice dell'Assemblea (con i miei commenti)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols

Che ne dici di ottimizzazione attivata?
serv-inc,

2
@utente: Probabilmente nessun cambiamento, ma ti aspetti che il bit-twiddler torni presto?
Deduplicatore,

2
Attenzione: mentre in C non ci sono tipi definiti dall'utente con operatori sovraccarichi, in C ++ ce ne sono e la generalizzazione dai tipi di base ai tipi definiti dall'utente non è semplicemente valida .
Deduplicatore,

@Deduplicator: Grazie, anche per aver sottolineato che questa risposta non si generalizza ai tipi definiti dall'utente. Non avevo guardato la sua pagina utente prima di chiedere.
serv-inc,

12

Non preoccuparti delle prestazioni, ad esempio il 97% delle volte. L'ottimizzazione precoce è la radice di tutto il male.

- Donald Knuth

Ora che questo è fuori dalla nostra strada, facciamo la nostra scelta in modo sano :

  • ++i: incrementa prefisso , incrementa il valore corrente e produce il risultato
  • i++: incremento postfisso , copia il valore, incrementa il valore corrente, produce la copia

A meno che non sia necessaria una copia del vecchio valore, l'uso dell'incremento postfix è un modo per ottenere risultati.

L'inesattezza deriva dalla pigrizia, usa sempre il costrutto che esprime il tuo intento nel modo più diretto, ci sono meno possibilità di quanto il futuro manutentore possa fraintendere il tuo intento originale.

Anche se qui è (davvero) minore, ci sono momenti in cui sono stato davvero perplesso leggendo il codice: mi chiedevo davvero se l'intento e l'espresso reale coincidessero, e ovviamente, dopo alcuni mesi, loro (o io) non ricordavo neanche ...

Quindi, non importa se ti sembra giusto o no. Abbraccio KISS . In pochi mesi avrai evitato le tue vecchie pratiche.


4

In C ++, tu potrebbe fare una differenza di prestazioni sostanziale se ci sono sovraccarichi gli operatori coinvolti, soprattutto se si sta scrittura di codice basato su modelli e non si sa cosa iteratori potrebbe essere passato. La logica dietro qualsiasi iteratore X può essere sia sostanziale e significativa: cioè lento e non ottimizzabile dal compilatore.

Ma questo non è il caso in C, dove sai che sarà solo un tipo banale, e la differenza di prestazioni è banale e il compilatore può facilmente ottimizzare via.

Quindi un consiglio: programmate in C, o in C ++, e le domande riguardano l'una o l'altra, non entrambe.


2

Le prestazioni di entrambe le operazioni dipendono fortemente dall'architettura sottostante. Bisogna incrementare un valore archiviato in memoria, il che significa che il collo di bottiglia di von Neumann è il fattore limitante in entrambi i casi.

Nel caso di ++ i, dobbiamo

Fetch i from memory 
Increment i
Store i back to memory
Use i

Nel caso di i ++, dobbiamo

Fetch i from memory
Use i
Increment i
Store i back to memory

Gli operatori ++ e - tracciano la loro origine nel set di istruzioni PDP-11. Il PDP-11 potrebbe eseguire il post-incremento automatico su un registro. Potrebbe anche eseguire il pre-decremento automatico su un indirizzo efficace contenuto in un registro. In entrambi i casi, il compilatore poteva sfruttare queste operazioni a livello di macchina solo se la variabile in questione era una variabile "registro".


2

Se vuoi sapere se qualcosa è lento, provalo. Prendi un BigInteger o un equivalente, incollalo in un ciclo simile per entrambi i modi di dire loop, assicurati che l'interno del loop non sia ottimizzato e cronometra entrambi.

Dopo aver letto l'articolo, non lo trovo molto convincente, per tre motivi. Primo, il compilatore dovrebbe essere in grado di ottimizzare la creazione di un oggetto che non viene mai utilizzato. Due, il i++concetto è idiomatico per i numerici per i loop , quindi i casi che vedo effettivamente interessati sono limitati a. Tre, forniscono un argomento puramente teorico, senza numeri per sostenerlo.

Soprattutto in base al motivo n. 1, la mia ipotesi è che quando si eseguono effettivamente i tempi, saranno proprio uno accanto all'altro.


-1

Innanzitutto non influisce sulla leggibilità dell'IMO. Non è quello che eri abituato a vedere, ma passerebbe poco prima che ti abitui.

Secondo, a meno che tu non usi un sacco di operatori postfix nel tuo codice, probabilmente non vedrai molte differenze. L'argomento principale per non usarli quando possibile è che una copia del valore del var originale deve essere conservata fino alla fine degli argomenti in cui il var originale potrebbe ancora essere usato. Sono 32 bit o 64 bit a seconda dell'architettura. Ciò equivale a 4 o 8 byte o 0,00390625 o 0,0078125 MB. Le probabilità sono molto alte che a meno che tu non ne usi un sacco, che devono essere salvate per un periodo di tempo molto lungo, che con le risorse e la velocità del computer di oggi non noteresti alcuna differenza passando da postfisso a prefisso.

EDIT: Dimentica questa parte rimanente poiché la mia conclusione è stata dimostrata falsa (tranne per la parte di ++ i e i ++ che non fa sempre la stessa cosa ... è ancora vero).

Inoltre è stato sottolineato in precedenza che non fanno la stessa cosa in un caso. Fai attenzione a fare il cambio se decidi di farlo. Non l'ho mai provato (ho sempre usato postfix) quindi non lo so per certo ma penso che passare da postfix a prefisso comporterà risultati diversi: (di nuovo potrei sbagliarmi ... dipende dal compilatore / anche interprete)

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}

4
L'operazione di incremento si verifica alla fine del ciclo for, quindi avrebbero lo stesso output esatto. Non dipende dal compilatore / interprete.
jsternberg

@jsternberg ... Grazie non ero sicuro di quando l'incremento fosse avvenuto poiché non avevo mai avuto motivo di provarlo. È passato troppo tempo da quando ho fatto i compilatori al college! lol
Kenneth

Sbagliato, sbagliato, sbagliato.
ruohola,

-1

Penso semanticamente, ++iha più senso di i++così, quindi mi atterrei al primo, tranne che è comune non farlo (come in Java, dove dovresti usare i++perché è ampiamente usato).


-2

Non si tratta solo di prestazioni.

A volte vuoi evitare di implementare la copia, perché non ha senso. E poiché l'uso dell'incremento del prefisso non dipende da questo, è chiaramente più semplice attenersi alla forma del prefisso.

E usare incrementi diversi per tipi primitivi e tipi complessi ... è davvero illeggibile.


-2

A meno che tu non ne abbia davvero bisogno, mi limiterei a ++ i. Nella maggior parte dei casi, questo è ciò che si intende. Non molto spesso hai bisogno di i ++ e devi sempre pensarci due volte quando leggi un tale costrutto. Con ++ i, è facile: aggiungi 1, lo usi e poi sono sempre lo stesso.

Quindi, sono pienamente d'accordo con @martin beckett: rendilo più facile per te, è già abbastanza difficile.

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.