Esiste una differenza di prestazioni tra i ++ e ++ i in C ++?

Abbiamo la domanda c‘è una differenza di prestazioni tra i++ e ++i in C ?

Qual è la risposta per C ++?

[Riepilogo esecutivo: usa ++i se non hai una ragione specifica per usare i++ .]

Per C ++, la risposta è un po ‘più complicata.

Se i è un tipo semplice (non un’istanza di una class C ++), la risposta data per C (“No non c’è alcuna differenza di prestazioni”) vale, dal momento che il compilatore sta generando il codice.

Tuttavia, se i è un’istanza di una class C ++, quindi i++ e ++i stanno effettuando chiamate a una delle funzioni operator++ . Ecco una coppia standard di queste funzioni:

 Foo& Foo::operator++() // called for ++i { this->data += 1; return *this; } Foo Foo::operator++(int ignored_dummy_value) // called for i++ { Foo tmp(*this); // variable "tmp" cannot be optimized away by the compiler ++(*this); return tmp; } 

Poiché il compilatore non sta generando codice, ma sta semplicemente chiamando una funzione di operator++ , non c’è modo di ottimizzare la variabile tmp e il suo costruttore di copie associato. Se il costruttore di copie è costoso, questo può avere un impatto significativo sulle prestazioni.

Sì. C’è.

L’operatore ++ può o non può essere definito come una funzione. Per i tipi primitivi (int, double, …) gli operatori sono integrati, quindi il compilatore sarà probabilmente in grado di ottimizzare il tuo codice. Ma nel caso di un object che definisce l’operatore ++ le cose sono diverse.

La funzione ++ (int) dell’operatore deve creare una copia. Questo perché ci si aspetta che postfix ++ restituisca un valore diverso da quello che contiene: deve mantenere il suo valore in una variabile temporanea, incrementarne il valore e restituire la temp. Nel caso dell’operatore ++ (), prefisso ++, non è necessario creare una copia: l’object può incrementare se stesso e quindi semplicemente restituire se stesso.

Ecco un’illustrazione del punto:

 struct C { C& operator++(); // prefix C operator++(int); // postfix private: int i_; }; C& C::operator++() { ++i_; return *this; // self, no copy created } CC::operator++(int ignored_dummy_value) { C t(*this); ++(*this); return t; // return a copy } 

Ogni volta che chiami l’operatore ++ (int) devi creare una copia e il compilatore non può fare nulla al riguardo. Quando viene data la scelta, usa operator ++ (); in questo modo non si salva una copia. Potrebbe essere significativo nel caso di molti incrementi (loop di grandi dimensioni?) E / o oggetti di grandi dimensioni.

Ecco un punto di riferimento per il caso in cui gli operatori di incremento si trovano in diverse unità di traduzione. Compilatore con g ++ 4.5.

Ignora i problemi di stile per ora

 // a.cc #include  #include  class Something { public: Something& operator++(); Something operator++(int); private: std::array data; }; int main () { Something s; for (int i=0; i<1024*1024*30; ++i) ++s; // warm up std::clock_t a = clock(); for (int i=0; i<1024*1024*30; ++i) ++s; a = clock() - a; for (int i=0; i<1024*1024*30; ++i) s++; // warm up std::clock_t b = clock(); for (int i=0; i<1024*1024*30; ++i) s++; b = clock() - b; std::cout << "a=" << (a/double(CLOCKS_PER_SEC)) << ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n'; return 0; } 

O (n) incremento

Test

 // b.cc #include  class Something { public: Something& operator++(); Something operator++(int); private: std::array data; }; Something& Something::operator++() { for (auto it=data.begin(), end=data.end(); it!=end; ++it) ++*it; return *this; } Something Something::operator++(int) { Something ret = *this; ++*this; return ret; } 

risultati

Risultati (i tempi sono in secondi) con g ++ 4.5 su una macchina virtuale:

 Flags (--std=c++0x) ++i i++ -DPACKET_SIZE=50 -O1 1.70 2.39 -DPACKET_SIZE=50 -O3 0.59 1.00 -DPACKET_SIZE=500 -O1 10.51 13.28 -DPACKET_SIZE=500 -O3 4.28 6.82 

O (1) incremento

Test

Prendiamo ora il seguente file:

 // c.cc #include  class Something { public: Something& operator++(); Something operator++(int); private: std::array data; }; Something& Something::operator++() { return *this; } Something Something::operator++(int) { Something ret = *this; ++*this; return ret; } 

Non fa nulla nell'incremento. Questo simula il caso in cui l'incremento ha una complessità costante.

risultati

I risultati ora variano estremamente:

 Flags (--std=c++0x) ++i i++ -DPACKET_SIZE=50 -O1 0.05 0.74 -DPACKET_SIZE=50 -O3 0.08 0.97 -DPACKET_SIZE=500 -O1 0.05 2.79 -DPACKET_SIZE=500 -O3 0.08 2.18 -DPACKET_SIZE=5000 -O3 0.07 21.90 

Conclusione

Performance-wise

Se non hai bisogno del valore precedente, prendi l'abitudine di usare il pre-incremento. Siate coerenti anche con i tipi built-in, vi abituerete e non correrete il rischio di subire perdite di prestazioni inutili se sostituirete mai un tipo built-in con un tipo personalizzato.

Semantic-saggio

  • i++ dice increment i, I am interested in the previous value, though .
  • ++i dice increment i, I am interested in the current value o increment i, no interest in the previous value . Di nuovo ti abituerai, anche se non sei in questo momento.

Knuth.

L'ottimizzazione prematura è la radice di tutto il male. Come è prematura la pessimizzazione.

Non è del tutto corretto dire che il compilatore non può ottimizzare la copia temporanea della variabile nel caso postfisso. Un rapido test con VC dimostra che, almeno, può farlo in alcuni casi.

Nell’esempio seguente, il codice generato è identico per prefisso e postfix, ad esempio:

 #include  class Foo { public: Foo() { myData=0; } Foo(const Foo &rhs) { myData=rhs.myData; } const Foo& operator++() { this->myData++; return *this; } const Foo operator++(int) { Foo tmp(*this); this->myData++; return tmp; } int GetData() { return myData; } private: int myData; }; int main(int argc, char* argv[]) { Foo testFoo; int count; printf("Enter loop count: "); scanf("%d", &count); for(int i=0; i 

Sia che eseguiate ++ testFoo o testFoo ++, otterrete comunque lo stesso codice risultante. In effetti, senza aver letto il conteggio in ingresso da parte dell'utente, l'ottimizzatore ha ridotto tutto a una costante. Così questo:

 for(int i=0; i<10; i++) { testFoo++; } printf("Value: %d\n", testFoo.GetData()); 

Risultato nel seguente:

 00401000 push 0Ah 00401002 push offset string "Value: %d\n" (402104h) 00401007 call dword ptr [__imp__printf (4020A0h)] 

Quindi, anche se è certamente il caso che la versione postfix potrebbe essere più lenta, potrebbe essere che l'ottimizzatore sia abbastanza buono da eliminare la copia temporanea se non la usi.

La guida di stile di Google C ++ dice:

Preincremento e Predecrement

Utilizzare il modulo prefisso (++ i) degli operatori di incremento e decremento con iteratori e altri oggetti modello.

Definizione: Quando una variabile viene incrementata (++ i o i ++) o decrementata (–i o i–) e il valore dell’espressione non viene utilizzato, si deve decidere se preincrementare (decrementare) o postincrementare (decrementare).

Vantaggi: quando il valore di ritorno viene ignorato, il formato “pre” (++ i) non è mai meno efficiente del modulo “post” (i ++) ed è spesso più efficiente. Questo perché il post-incremento (o decremento) richiede una copia di i, che è il valore dell’espressione. Se sono un iteratore o un altro tipo non scalare, copiare potrebbe essere costoso. Dal momento che i due tipi di incremento si comportano allo stesso modo quando il valore viene ignorato, perché non solo sempre pre-incrementare?

Contro: La tradizione sviluppata, in C, sull’uso del post-incremento quando il valore dell’espressione non è usato, specialmente nei cicli for. Alcuni trovano più facile leggere il post-incremento, poiché il “sobject” (i) precede il “verbo” (++), proprio come in inglese.

Decisione: per valori scalari semplici (non object) non c’è motivo di preferire un modulo e consentiamo entrambi. Per gli iteratori e altri tipi di modelli, utilizzare pre-incremento.

Vorrei sottolineare un messaggio eccellente di Andrew Koenig su Code Talk molto recentemente.

http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29

Nella nostra azienda usiamo anche la convenzione di ++ iter per coerenza e prestazioni ove applicabile. Ma Andrew solleva dettagli troppo dettagliati riguardo l’intento e le prestazioni. Ci sono volte in cui vogliamo usare iter ++ anziché ++ iter.

Quindi, per prima cosa decidi il tuo intento e se il pre o post non ha importanza, allora vai con il pre in quanto avrà dei vantaggi in termini di prestazioni evitando la creazione di oggetti extra e lanciandolo.

@Ketan

… solleva dettagli eccessivi riguardo l’intento e le prestazioni. Ci sono volte in cui vogliamo usare iter ++ anziché ++ iter.

Ovviamente post e pre-incremento hanno semantica diversa e sono sicuro che tutti sono d’accordo che quando si utilizza il risultato si dovrebbe usare l’operatore appropriato. Penso che la domanda sia: cosa si dovrebbe fare quando il risultato viene scartato (come nei loop). La risposta a questa domanda (IMHO) è che, dal momento che le considerazioni sulla performance sono trascurabili al meglio, dovresti fare ciò che è più naturale. Per me ++i è più naturale, ma la mia esperienza mi dice che sono in minoranza e l’utilizzo di i++ causerà meno sovraccarico di metallo per la maggior parte delle persone che leggono il tuo codice.

Dopo tutto, questo è il motivo per cui il linguaggio non è chiamato ” ++C “. [*]

[*] Inserisci una discussione obbligatoria su ++C essendo un nome più logico.

Mark: Volevo solo sottolineare che gli operatori ++ sono buoni candidati per essere inline, e se il compilatore decide di farlo, la copia ridondante verrà eliminata nella maggior parte dei casi. (ad esempio, i tipi di POD, che di solito sono gli iteratori).

Detto questo, nella maggior parte dei casi è ancora meglio usare l’iter ++. 🙂

La differenza di prestazioni tra ++i e i++ sarà più evidente quando pensate agli operatori come a funzioni che restituiscono valore e in che modo vengono implementate. Per rendere più facile capire cosa sta succedendo, i seguenti esempi di codice useranno int come se fosse una struct .

++i incrementa la variabile, quindi restituisce il risultato. Questo può essere fatto sul posto e con un tempo di CPU minimo, richiedendo solo una riga di codice in molti casi:

 int& int::operator++() { return *this += 1; } 

Ma lo stesso non si può dire di i++ .

Il post-incremento, i++ , è spesso visto come restituire il valore originale prima di incrementare. Tuttavia, una funzione può restituire un risultato solo quando è finito . Di conseguenza, diventa necessario creare una copia della variabile contenente il valore originale, incrementare la variabile, quindi restituire la copia contenente il valore originale:

 int int::operator++(int& _Val) { int _Original = _Val; _Val += 1; return _Original; } 

Quando non vi è alcuna differenza funzionale tra pre-incremento e post-incremento, il compilatore può eseguire l’ottimizzazione in modo tale che non vi sia alcuna differenza di prestazioni tra i due. Tuttavia, se è coinvolto un tipo di dati composito come una struct o una class , il costruttore di copie verrà richiamato dopo l’incremento e non sarà ansible eseguire questa ottimizzazione se è necessaria una copia profonda. Di conseguenza, il pre-incremento generalmente è più veloce e richiede meno memoria rispetto al post-incremento.

  1. ++ i – più veloce non usando il valore di ritorno
  2. i ++ : più veloce usando il valore di ritorno

Quando non si utilizza il valore di ritorno, il compilatore è garantito per non utilizzare un temporaneo nel caso di ++ i . Non è garantito che sia più veloce, ma è garantito che non sia più lento.

Quando si utilizza il valore restituito, i ++ consente al processore di spostare sia l’incremento che il lato sinistro nella pipeline poiché non dipendono l’uno dall’altro. ++ Potrei bloccare la pipeline perché il processore non può avviare il lato sinistro finché l’operazione di pre-incremento non è iniziata a meandri. Di nuovo, uno stallo della pipeline non è garantito, poiché il processore potrebbe trovare altre cose utili da inserire.

@ Mark: Ho cancellato la mia risposta precedente perché era un po ‘capovolta, e meritavo un downvote solo per quello. In realtà penso che sia una buona domanda nel senso che chiede cosa c’è nella mente di molte persone.

La solita risposta è che ++ i è più veloce di I ++, e senza dubbio lo è, ma la domanda più grande è “quando dovrebbe interessarti?”

Se la frazione di tempo della CPU impiegata per incrementare gli iteratori è inferiore al 10%, potrebbe non interessarti.

Se la frazione del tempo di CPU speso nell’incremento degli iteratori è superiore al 10%, puoi vedere quali istruzioni stanno facendo quella iterazione. Verifica se è ansible incrementare gli interi anziché utilizzare gli iteratori. È probabile che tu possa, e sebbene possa essere in qualche modo meno desiderabile, le possibilità sono buone, risparmierai sostanzialmente tutto il tempo trascorso in quegli iteratori.

Ho visto un esempio in cui l’incremento dell’iteratore stava consumando oltre il 90% delle volte. In tal caso, passando all’intero-incrementando il tempo di esecuzione ridotto essenzialmente di tale importo. (vale a dire migliore di 10x di accelerazione)

La domanda prevista riguardava quando il risultato è inutilizzato (ciò è chiaro dalla domanda per C). Qualcuno può risolvere questo problema poiché la domanda è “wiki della comunità”?

A proposito di ottimizzazioni premature, spesso viene citata Knuth. Giusto. ma Donald Knuth non avrebbe mai difeso con questo il codice orribile che puoi vedere in questi giorni. Mai visto a = b + c tra Java Integer (non int)? Ciò equivale a 3 conversioni di boxing / unboxing. Evitare cose del genere è importante. E scrivere inutilmente i ++ invece di ++ i è lo stesso errore. EDIT: Come phresnel lo inserisce in un commento, questo può essere riassunto come “l’ottimizzazione prematura è malvagia, così come la premessa pessimizzazione”.

Anche il fatto che le persone siano più abituate a i ++ è una sfortunata eredità C, causata da un errore concettuale di K & R (se segui l’argomento intent, questa è una conclusione logica, e difendere K & R perché sono K & R non ha significato, sono grandioso, ma non sono grandi come progettisti di linguaggi, esistono innumerevoli errori nel design C, che vanno da gets () a strcpy (), all’API strncpy () (avrebbe dovuto avere l’API strlcpy () dal giorno 1) ).

Btw, io sono uno di quelli non usati abbastanza per il C ++ per trovare ++ ho fastidioso leggere. Comunque, lo uso da quando riconosco che è giusto.

@wilhelmtell

Il compilatore può elidere il temporaneo. Verbatim dall’altra discussione:

Il compilatore C ++ è autorizzato ad eliminare i temporanei basati sullo stack anche se così facendo cambia il comportamento del programma. Collegamento MSDN per VC 8:

http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx

Un motivo per cui dovresti usare ++ i anche su tipi built-in in cui non ci sono vantaggi prestazionali è quello di creare una buona abitudine per te stesso.

Entrambe sono altrettanto veloci;) Se lo si desidera è lo stesso calcolo per il processore, è solo l’ordine in cui viene eseguito che differiscono.

Ad esempio, il seguente codice:

 #include  int main() { int a = 0; a++; int b = 0; ++b; return 0; } 

Produrre il seguente assemblaggio:

  0x0000000100000f24 : push %rbp 0x0000000100000f25 : mov %rsp,%rbp 0x0000000100000f28 : movl $0x0,-0x4(%rbp) 0x0000000100000f2f : incl -0x4(%rbp) 0x0000000100000f32 : movl $0x0,-0x8(%rbp) 0x0000000100000f39 : incl -0x8(%rbp) 0x0000000100000f3c : mov $0x0,%eax 0x0000000100000f41 : leaveq 0x0000000100000f42 : retq 

Vedete che per a ++ e b ++ è un mnemonico incl, quindi è la stessa operazione;)

È ora di fornire alle persone gemme di saggezza;) – c’è un semplice trucco per far sì che l’incremento postfix del C ++ si comporti più o meno come l’incremento del prefisso (l’ho inventato per me stesso, ma l’ho visto anche nel codice di altre persone, quindi non lo sono solo).

Fondamentalmente, il trucco è usare la class helper per posticipare l’incremento dopo il ritorno, e RAII viene in soccorso

 #include  class Data { private: class DataIncrementer { private: Data& _dref; public: DataIncrementer(Data& d) : _dref(d) {} public: ~DataIncrementer() { ++_dref; } }; private: int _data; public: Data() : _data{0} {} public: Data(int d) : _data{d} {} public: Data(const Data& d) : _data{ d._data } {} public: Data& operator=(const Data& d) { _data = d._data; return *this; } public: ~Data() {} public: Data& operator++() { // prefix ++_data; return *this; } public: Data operator++(int) { // postfix DataIncrementer t(*this); return *this; } public: operator int() { return _data; } }; int main() { Data d(1); std::cout << d << '\n'; std::cout << ++d << '\n'; std::cout << d++ << '\n'; std::cout << d << '\n'; return 0; } 

Inventato è per alcuni codici iteratori personalizzati pesanti e riduce il tempo di esecuzione. Il costo del prefisso vs postfix è ora un riferimento, e se questo è un operatore personalizzato che fa molto movimento, prefisso e postfix hanno dato lo stesso tempo di esecuzione per me.

Quando scrivi i++ stai dicendo al compilatore di incrementare dopo aver completato questa linea o loop.

++i è un po ‘diverso da i++ . In i++ si incrementa dopo aver terminato il ciclo, ma ++i si incrementano direttamente prima del termine del ciclo.

++i è più veloce di i++ perché non restituisce una vecchia copia del valore.

È anche più intuitivo:

 x = i++; // x contains the old value of i y = ++i; // y contains the new value of i 

Questo esempio C stampa “02” anziché il “12” che potresti aspettarti:

 #include  int main(){ int a = 0; printf("%d", a++); printf("%d", ++a); return 0; } 

Lo stesso per C ++ :

 #include  using namespace std; int main(){ int a = 0; cout << a++; cout << ++a; return 0; }