Abbiamo la domanda : esiste una differenza di prestazioni tra i++
e ++i
in C ?
Qual è la risposta per C ++?
Abbiamo la domanda : esiste una differenza di prestazioni tra i++
e ++i
in C ?
Qual è la risposta per C ++?
Risposte:
[Riepilogo esecutivo: utilizzare ++i
se non si dispone di un motivo specifico da utilizzare i++
.]
Per C ++, la risposta è un po 'più complicata.
Se i
è un tipo semplice (non un'istanza di una classe C ++), vale la risposta data per C ("No non c'è differenza di prestazioni") , poiché il compilatore sta generando il codice.
Tuttavia, se i
è un'istanza di una classe C ++, quindi i++
e ++i
sta effettuando chiamate a una delle operator++
funzioni. 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 operator++
funzione, non c'è modo di ottimizzare la tmp
variabile e il suo costruttore di copie associato. Se il costruttore di copie è costoso, ciò 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 oggetto che definisce l'operatore ++ le cose sono diverse.
La funzione operator ++ (int) deve creare una copia. Questo perché postfix ++ dovrebbe restituire un valore diverso da quello che contiene: deve contenere il suo valore in una variabile temp, incrementare il suo valore e restituire la temp. Nel caso di operator ++ (), prefisso ++, non è necessario creare una copia: l'oggetto 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
}
C C::operator++(int ignored_dummy_value)
{
C t(*this);
++(*this);
return t; // return a copy
}
Ogni volta che chiami l'operatore ++ (int) devi crearne una copia e il compilatore non può farci nulla. Quando viene data la scelta, utilizzare operator ++ (); in questo modo non si salva una copia. Potrebbe essere significativo nel caso di molti incrementi (anello grande?) E / o oggetti di grandi dimensioni.
C t(*this); ++(*this); return t;
Nella seconda riga, stai incrementando questo puntatore a destra, quindi come t
si aggiorna se lo stai incrementando. I valori di questo non erano già stati copiati t
?
The operator++(int) function must create a copy.
no non lo è. Non più copie dioperator++()
Ecco un punto di riferimento per il caso in cui gli operatori di incremento si trovano in unità di traduzione diverse. Compilatore con g ++ 4.5.
Ignora i problemi di stile per ora
// a.cc
#include <ctime>
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> 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;
}
// b.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> 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 (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
Prendiamo ora il seguente file:
// c.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> 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.
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
Se non è necessario il valore precedente, prendere l'abitudine di utilizzare il pre-incremento. Siate coerenti anche con i tipi predefiniti, vi abituerete e non correrete il rischio di subire inutili perdite di prestazioni se sostituite un tipo incorporato con un tipo personalizzato.
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
. Ancora una volta, ti abituerai, anche se non lo sei adesso.L'ottimizzazione prematura è la radice di tutti i mali. Come la pessimizzazione prematura.
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
non preoccuparti dell'attuale struttura ad albero (BSP, kd, Quadtree, Octree Grid, ecc.). Tale un iteratore avrebbe bisogno di mantenere uno stato, ad esempio parent node
, child node
, index
e cose del genere. Tutto sommato, la mia posizione è, anche se esistono solo pochi esempi, ...
Non è del tutto corretto affermare che il compilatore non può ottimizzare la copia della variabile temporanea nel caso postfix. Un test rapido con VC mostra che, almeno, in alcuni casi può farlo.
Nel seguente esempio, il codice generato è identico per prefisso e postfisso, ad esempio:
#include <stdio.h>
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<count; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
}
Che tu faccia ++ testFoo o testFoo ++, otterrai comunque lo stesso codice risultante. In effetti, senza leggere il conteggio dell'utente, l'ottimizzatore ha portato il tutto a una costante. Così questo:
for(int i=0; i<10; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
Ha provocato quanto segue:
00401000 push 0Ah
00401002 push offset string "Value: %d\n" (402104h)
00401007 call dword ptr [__imp__printf (4020A0h)]
Quindi, mentre è certamente il caso che la versione postfix potrebbe essere più lenta, potrebbe anche essere che l'ottimizzatore sia abbastanza buono da sbarazzarsi della copia temporanea se non la si utilizza.
La Guida allo stile di Google C ++ dice:
Preincrement 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, è necessario decidere se preincrementare (decremento) o postincremento (decremento).
Pro: Quando il valore restituito viene ignorato, il modulo "pre" (++ i) non è mai meno efficiente del modulo "post" (i ++) ed è spesso più efficiente. Questo perché post-incremento (o decremento) richiede una copia di i, che è il valore dell'espressione. Se sono un iteratore o un altro tipo non scalare, la copia potrebbe essere costosa. Dato che i due tipi di incremento si comportano allo stesso modo quando il valore viene ignorato, perché non pre-incrementare sempre?
Contro: la tradizione sviluppata, in C, sull'uso del post-incremento quando non si usa il valore dell'espressione, specialmente per i loop. Alcuni trovano che post-incremento sia più facile da leggere, poiché il "soggetto" (i) precede il "verbo" (++), proprio come in inglese.
Decisione: per semplici valori scalari (non oggetto) non c'è motivo di preferire un modulo e ne consentiamo uno dei due. Per gli iteratori e altri tipi di modello, utilizzare il pre-incremento.
Vorrei sottolineare recentemente un eccellente post di Andrew Koenig su Code Talk.
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
Nella nostra azienda utilizziamo anche la convenzione di ++ iter per coerenza e prestazioni ove applicabile. Ma Andrew fa emergere dettagli trascurati riguardo a intenti e prestazioni. Ci sono momenti in cui vogliamo usare iter ++ anziché ++ iter.
Quindi, prima decidi il tuo intento e se pre o post non contano, vai con pre in quanto avrà qualche vantaggio in termini di prestazioni evitando la creazione di oggetti extra e lanciandoli.
@Ketan
... aumenta i dettagli trascurati per quanto riguarda l'intento contro le prestazioni. Ci sono momenti in cui vogliamo usare iter ++ anziché ++ iter.
Ovviamente post e pre-increment hanno una semantica diversa e sono sicuro che tutti concordano sul fatto che quando si utilizza il risultato è necessario utilizzare l'operatore appropriato. Penso che la domanda sia cosa si dovrebbe fare quando il risultato viene scartato (come nei for
loop). La risposta a questa domanda (IMHO) è che, poiché le considerazioni sulle prestazioni sono al massimo trascurabili, dovresti fare ciò che è più naturale. Per me ++i
è più naturale, ma la mia esperienza mi dice che sono in minoranza e l'utilizzo i++
causerà meno sovraccarico di metallo per la maggior parte delle persone che leggono il tuo codice.
Dopo tutto questo è il motivo per cui la lingua non è chiamata " ++C
". [*]
[*] Inserisci una discussione obbligatoria ++C
sull'essere un nome più logico.
Quando non si utilizza il valore restituito, si garantisce che il compilatore non utilizzi un valore temporaneo nel caso di ++ i . Non garantito per essere più veloce, ma garantito per non essere più lento.
Quando si utilizza il valore restituito i ++ consente al processore di inserire sia l'incremento che il lato sinistro nella pipeline poiché non dipendono l'uno dall'altro. ++ Potrei arrestare la pipeline perché il processore non può avviare il lato sinistro fino a quando l'operazione di pre-incremento non si è completamente interrotta. Ancora una volta, una stalla della pipeline non è garantita, dal momento che il processore può trovare altre cose utili su cui attaccare.
Mark: Volevo solo sottolineare che gli operatori ++ sono buoni candidati da inserire, e se il compilatore decide di farlo, la copia ridondante verrà eliminata nella maggior parte dei casi. (es. tipi di POD, che di solito sono iteratori.)
Detto questo, nella maggior parte dei casi è ancora meglio usare ++ iter. :-)
La differenza di prestazioni tra ++i
e i++
sarà più evidente se si considerano gli operatori come funzioni di ritorno del valore e come sono implementate. Per semplificare la comprensione di ciò che sta accadendo, i seguenti esempi di codice verranno utilizzati int
come se fossero a struct
.
++i
incrementa la variabile, quindi restituisce il risultato. Questo può essere fatto sul posto e con un tempo CPU minimo, richiedendo solo una riga di codice in molti casi:
int& int::operator++() {
return *this += 1;
}
Ma lo stesso non si può dire i++
.
Post-incrementing, i++
è spesso visto come la restituzione del valore originale prima dell'incremento. Tuttavia, una funzione può restituire un risultato solo al termine . Di conseguenza, diventa necessario creare una copia della variabile contenente il valore originale, incrementare la variabile, quindi restituire la copia con 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 siano differenze di prestazioni tra i due. Tuttavia, se è coinvolto un tipo di dati composito come struct
o o class
, il costruttore della copia verrà chiamato in post-incremento e non sarà possibile eseguire questa ottimizzazione se è necessaria una copia approfondita. Pertanto, il pre-incremento è generalmente più veloce e richiede meno memoria rispetto al post-incremento.
@Mark: ho cancellato la mia risposta precedente perché era un po 'capovolta, e meritavo un voto negativo solo per quello. In realtà penso che sia una buona domanda, nel senso che si 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 nell'incrementare gli iteratori è inferiore al 10%, potrebbe non interessarti.
Se la frazione di tempo della CPU impiegata nell'incrementare gli iteratori è maggiore del 10%, puoi vedere quali istruzioni stanno eseguendo tale iterazione. Verifica se puoi semplicemente incrementare numeri interi anziché utilizzare iteratori. È probabile che tu possa, e mentre in un certo senso potrebbe essere meno desiderabile, è molto probabile che risparmierai essenzialmente tutto il tempo trascorso in quegli iteratori.
Ho visto un esempio in cui l'incremento dell'iteratore consumava ben oltre il 90% delle volte. In tal caso, passare all'incremento di numeri interi ha ridotto il tempo di esecuzione essenzialmente di tale importo. (ovvero migliore della velocità 10x)
@wilhelmtell
Il compilatore può eliminare il temporaneo. Verbatim dall'altra discussione:
Il compilatore C ++ può eliminare i provvisori basati su stack anche se in tal modo si modifica 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 predefiniti in cui non ci sono vantaggi prestazionali è creare una buona abitudine per te stesso.
Entrambi sono altrettanto veloci;) Se vuoi che sia lo stesso calcolo per il processore, è solo l'ordine in cui è fatto che differisce.
Ad esempio, il seguente codice:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
Produrre il seguente assieme:
0x0000000100000f24 <main+0>: push %rbp 0x0000000100000f25 <main+1>: mov %rsp,%rbp 0x0000000100000f28 <main+4>: movl $0x0,-0x4(%rbp) 0x0000000100000f2f <main+11>: incl -0x4(%rbp) 0x0000000100000f32 <main+14>: movl $0x0,-0x8(%rbp) 0x0000000100000f39 <main+21>: incl -0x8(%rbp) 0x0000000100000f3c <main+24>: mov $0x0,%eax 0x0000000100000f41 <main+29>: leaveq 0x0000000100000f42 <main+30>: retq
Vedete che per a ++ e b ++ è un incl mnemonico, quindi è la stessa operazione;)
La domanda prevista riguardava quando il risultato non è stato utilizzato (questo è chiaro dalla domanda per C). Qualcuno può risolvere questo problema poiché la domanda è "wiki della community"?
A proposito di ottimizzazioni premature, Knuth viene spesso citato. Giusto. ma Donald Knuth non difenderebbe mai con quel codice orribile che puoi vedere in questi giorni. Hai mai visto a = b + c tra Java Integers (non int)? Ciò equivale a 3 conversioni boxe / unboxing. Evitare cose del genere è importante. E scrivere inutilmente i ++ anziché ++ i è lo stesso errore. EDIT: Come dice bene Phresnel in un commento, questo può essere riassunto come "l'ottimizzazione prematura è malvagia, così come la pessimizzazione prematura".
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 intento, questa è una conclusione logica; e difendere K&R perché sono K&R non ha significato, sono fantastico, ma non sono grandi come designer del linguaggio; esistono innumerevoli errori nella progettazione C, che vanno da gets () a strcpy (), all'API strncpy () (avrebbe dovuto avere l'API strlcpy () dal primo giorno) ).
A proposito, sono uno di quelli che non sono abbastanza abituati a C ++ per trovare ++ che sono fastidioso da leggere. Tuttavia, lo uso poiché riconosco che è giusto.
++i
più fastidioso di i++
(in effetti, l'ho trovato più interessante), ma il resto del tuo post ottiene il mio pieno riconoscimento. Forse aggiungere un punto "l'ottimizzazione prematura è male, così come la pessimizzazione prematura"
strncpy
serviva a uno scopo nei filesystem che stavano usando in quel momento; il nome file era un buffer di 8 caratteri e non doveva essere terminato con null. Non puoi biasimarli per non aver visto 40 anni nel futuro dell'evoluzione del linguaggio.
strlcpy()
era giustificata dal fatto che non era ancora stata inventata.
È tempo di fornire alla gente gemme di saggezza;) - c'è un semplice trucco per fare in modo che l'incremento postfix C ++ si comporti più o meno come l'incremento prefisso (ho inventato questo per me stesso, ma l'ho visto anche nel codice di altre persone, quindi non lo sono solo).
Fondamentalmente, il trucco è usare la classe helper per posporre l'incremento dopo il ritorno, e RAII viene in soccorso
#include <iostream>
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 un codice iteratore personalizzato pesante e riduce il tempo di esecuzione. Il costo del prefisso rispetto al postfisso ora è un riferimento e se questo è un operatore personalizzato che si sta muovendo pesantemente, il prefisso e il postfisso hanno prodotto lo stesso tempo di esecuzione per me.
++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é "12" che potresti aspettarti:
#include <stdio.h>
int main(){
int a = 0;
printf("%d", a++);
printf("%d", ++a);
return 0;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}