Domanda

Abbiamo la domanda c'è una differenza di prestazioni tra i++ E ++i in C?

Qual è la risposta per C++?

È stato utile?

Soluzione

[Sintesi:Utilizzo ++i se non hai un motivo specifico per utilizzarlo i++.]

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

Se i è un tipo semplice (non un'istanza di una classe C++), quindi la risposta data per C ("No, non c'è differenza di prestazioni") vale, poiché il compilatore sta generando il codice.

Tuttavia, se i è quindi un'istanza di una classe C++ i++ E ++i stanno effettuando chiamate a uno dei 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 an operator++ funzione, non c'è modo di ottimizzare il file tmp variabile e il suo costruttore di copia associato.Se il costruttore della copia è costoso, ciò può avere un impatto significativo sulle prestazioni.

Altri suggerimenti

SÌ.C'è.

L'operatore ++ può o meno essere definito come una funzione.Per i tipi primitivi (int, double, ...) gli operatori sono incorporati, 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é si prevede 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 temperatura.Nel caso di operator++(), prefisso ++, non è necessario crearne una copia:l'oggetto può incrementarsi e poi semplicemente restituire se stesso.

Ecco un esempio 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 operator++(int) devi crearne una copia e il compilatore non può fare nulla al riguardo.Quando viene data la scelta, utilizzare operator++();in questo modo non ne salvi 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 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;
}

Incremento O(n).

Test

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

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

Incremento O(1).

Test

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.

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

Dal punto di vista delle prestazioni

Se non è necessario il valore precedente, prendere l'abitudine di utilizzare il pre-incremento.Sii coerente anche con i tipi integrati, ti abituerai e non correrai il rischio di subire inutili perdite di prestazioni se sostituirai un tipo integrato con un tipo personalizzato.

Dal punto di vista semantico

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

Knut.

L’ottimizzazione prematura è la radice di tutti i mali.Così come la pessimizzazione prematura.

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

Nell'esempio seguente, 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());
}

Sia che tu faccia ++testFoo o testFoo++, otterrai comunque lo stesso codice risultante.Infatti, senza leggere il conteggio da parte dell'utente, l'ottimizzatore ha ridotto il tutto a una costante.Così questo:

for(int i=0; i<10; i++)
{
    testFoo++;
}

printf("Value: %d\n", testFoo.GetData());

Il risultato è quanto segue:

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

Quindi, anche se è certamente vero che la versione postfix potrebbe essere più lenta, è possibile che l'ottimizzatore sia abbastanza buono da eliminare la copia temporanea se non la stai utilizzando.

IL Guida allo stile di Google C++ dice:

Preincremento e Predecremento

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 decrementato (--I o I--) e il valore dell'espressione non viene utilizzato, si deve decidere se preincremento (decremento) o post-incremento (decremento).

Professionisti: Quando il valore di ritorno 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 da realizzare una copia di I, che è il valore dell'espressione.Se io sono un iteratore o altro tipo non scalare, copiando potrei essere costoso.Poiché i due tipi di incremento si comportano allo stesso modo quando il valore viene ignorato, perché non sempre pre-incremento?

Contro: La tradizione si è sviluppata, in C, di utilizzare post-incremento quando il valore dell'espressione non viene utilizzato, specialmente per i loop.Alcuni trovano più facile da leggere dopo l'incremento, poiché il "soggetto" (i) precede il "verbo" (++), proprio come in inglese.

Decisione: Per semplici valori scalari (non oggetto) non vi è alcun motivo per preferire una forma e consentiremo neanche.Per iteratori e altri tipi di modelli, utilizzare la pre-incremento.

Vorrei sottolineare un eccellente post di Andrew Koenig su Code Talk di recente.

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

Nella nostra azienda utilizziamo inoltre la convenzione di ++iter per coerenza e prestazioni, ove applicabile.Ma Andrew solleva dettagli trascurati riguardo all'intento rispetto alla prestazione.Ci sono momenti in cui vogliamo usare iter++ invece di ++iter.

Quindi, prima decidi il tuo intento e se pre o post non hanno importanza, allora vai con pre poiché avrà qualche vantaggio in termini di prestazioni evitando la creazione di oggetti extra e lanciandoli.

@Ketan

...eleva dettagli trascurati relativi all'intento rispetto alla performance.Ci sono momenti in cui vogliamo usare iter++ invece di ++iter.

Ovviamente post e pre-incremento hanno una semantica diversa e sono sicuro che tutti concordano sul fatto che quando viene utilizzato il risultato è necessario utilizzare l'operatore appropriato.Penso che la domanda sia cosa si dovrebbe fare quando il risultato viene scartato (come in for cicli).La risposta a Questo La domanda (IMHO) è che, poiché le considerazioni sulle prestazioni sono nella migliore delle ipotesi trascurabili, dovresti fare ciò che è più naturale.Per me ++i è più naturale ma la mia esperienza mi dice che sono in minoranza e ne faccio uso i++ causerà meno spese generali di metallo maggior parte persone che leggono il tuo codice.

Dopotutto è per questo che la lingua non si chiama "++C".[*]

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

Segno:Volevo solo sottolineare che gli operator++ sono buoni candidati per essere incorporati e, se il compilatore sceglie di farlo, nella maggior parte dei casi la copia ridondante verrà eliminata.(per esempio.Tipi POD, quali sono solitamente gli iteratori.)

Detto questo, nella maggior parte dei casi è comunque preferibile utilizzare ++iter.:-)

La differenza di prestazioni tra ++i E i++ sarà più evidente se si pensa agli operatori come a funzioni che restituiscono valore e al modo in cui vengono implementati.Per rendere più semplice la comprensione di ciò che sta accadendo, verranno utilizzati i seguenti esempi di codice int come se fosse un struct.

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

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

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

Post-incremento, i++, viene spesso visto come se restituisse il valore originale Prima incrementale.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 che contiene il valore originale:

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

Quando non esiste 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 un tipo di dati composito come a struct O class è coinvolta, il costruttore della copia verrà chiamato al post-incremento e non sarà possibile eseguire questa ottimizzazione se è necessaria una copia profonda.Pertanto, il pre-incremento è generalmente più veloce e richiede meno memoria rispetto al post-incremento.

  1. ++i - Più veloce non utilizzare il valore restituito
  2. i++ - Più veloce utilizzando il valore restituito

Quando non utilizzare il valore restituito è garantito che il compilatore non utilizzi un valore temporaneo nel caso di ++i.Non è garantito che sia più veloce, ma è garantito che non sarà più lento.

Quando utilizzando il valore restituito i++ Consente al processore di spingere sia l'incremento che il lato sinistro nella conduttura 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 è stata completata.Ancora una volta, uno stallo della pipeline non è garantito, poiché il processore potrebbe trovare altre cose utili da inserire.

Un motivo per cui dovresti usare ++i anche sui tipi integrati in cui non ci sono vantaggi in termini di prestazioni è creare una buona abitudine per te stesso.

@Segno:Ho cancellato la mia risposta precedente perché era un po' inconsueta e meritavo un voto negativo solo per questo.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 dovresti preoccupartene?"

Se la frazione di tempo della CPU spesa nell'incremento degli iteratori è inferiore al 10%, potrebbe non interessarti.

Se la frazione di tempo della CPU impiegata nell'incremento degli iteratori è maggiore del 10%, puoi vedere quali istruzioni stanno eseguendo tale iterazione.Vedi se puoi semplicemente incrementare i numeri interi invece di usare gli iteratori.È probabile che potresti, e sebbene possa essere in un certo senso 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% del tempo.In tal caso, il passaggio all'incremento di numeri interi ha ridotto il tempo di esecuzione essenzialmente di tale importo.(cioè.migliore di 10 volte l'accelerazione)

La domanda prevista riguardava quando il risultato non viene utilizzato (questo è chiaro dalla domanda per C).Qualcuno può risolvere questo problema poiché la domanda è "wiki della comunità"?

Per quanto riguarda le ottimizzazioni premature, viene spesso citato Knuth.Giusto.ma Donald Knuth non difenderebbe mai in questo modo l'orribile codice che vedete in questi giorni.Hai mai visto a = b + c tra gli interi Java (non int)?Ciò equivale a 3 conversioni boxing/unboxing.Evitare cose del genere è importante.E scrivere inutilmente i++ invece di ++i è lo stesso errore.MODIFICARE:Come phresnel afferma bene in un commento, ciò può essere riassunto come "l'ottimizzazione prematura è un male, così come lo è la pessimizzazione prematura".

Anche il fatto che le persone siano più abituate a i++ è una sfortunata eredità del C, causata da un errore concettuale di K&R (se segui l'argomento dell'intento, questa è una conclusione logica;e difendere K&R perché sono K&R non ha senso, sono fantastici, ma non sono eccezionali come progettisti del linguaggio;esistono innumerevoli errori nella progettazione C, che vanno da goes() a strcpy(), all'API strncpy() (avrebbe dovuto avere l'API strlcpy() dal giorno 1)).

A proposito, sono uno di quelli che non sono abbastanza abituati al C++ da trovare ++i fastidioso da leggere.Tuttavia, lo uso poiché riconosco che è giusto.

@wilhelmtell

Il compilatore può eliminare il temporaneo.Testualmente dall'altro thread:

Il compilatore C++ può eliminare i temporanei basati sullo stack anche se ciò modifica il comportamento del programma.Collegamento MSDN per VC 8:

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

È ora di fornire alla gente gemme di saggezza ;) - c'è un semplice trucco per fare in modo che l'incremento del suffisso C++ si comporti più o meno come l'incremento del prefisso (l'ho inventato per me, ma l'ho visto anche nel codice di altre persone, quindi non lo sono solo).

Fondamentalmente, il trucco è usare la classe helper per posticipare l'incremento dopo il reso, 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;
}

È stato inventato per alcuni codici iteratori personalizzati pesanti e riduce il tempo di esecuzione.Il costo del prefisso rispetto al postfisso è un riferimento ora e se si tratta di un operatore personalizzato che esegue spostamenti pesanti, prefisso e postfisso hanno prodotto lo stesso tempo di esecuzione per me.

Entrambi sono altrettanto veloci;) se lo si desidera è 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

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

Quando scrivi i++ stai dicendo al compilatore di incrementare dopo aver terminato questa riga o ciclo.

++i è un po' diverso da i++.In i++ incrementi dopo aver terminato il ciclo ma ++i incrementi direttamente prima che il ciclo finisca.

++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 in C stampa "02" invece del "12" che potresti aspettarti:

#include <stdio.h>

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

Lo stesso per C++:

#include <iostream>
using namespace std;

int main(){
    int a = 0;
    cout << a++;
    cout << ++a;
    return 0;
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top