Question

Nous avons la question y a-t-il une différence de performances entre i++ et ++i en C?

Quelle est la réponse pour le C++ ?

Était-ce utile?

La solution

[Résumé exécutif:Utiliser ++i si vous n'avez pas de raison spécifique d'utiliser i++.]

Pour le C++, la réponse est un peu plus compliquée.

Si i est un type simple (pas une instance d'une classe C++), puis la réponse donnée pour C ("Non, il n'y a pas de différence de performances") est valable, puisque le compilateur génère le code.

Toutefois, si i est une instance d'une classe C++, alors i++ et ++i vous appelez l'un des operator++ les fonctions.Voici une paire standard de ces fonctions :

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;
}

Puisque le compilateur ne génère pas de code, mais appelle simplement un operator++ fonction, il n'y a aucun moyen d'optimiser la tmp variable et son constructeur de copie associé.Si le constructeur de copie est coûteux, cela peut avoir un impact significatif sur les performances.

Autres conseils

Oui.Il y a.

L'opérateur ++ peut ou non être défini comme une fonction.Pour les types primitifs (int, double, ...) les opérateurs sont intégrés, donc le compilateur pourra probablement optimiser votre code.Mais dans le cas d’un objet qui définit l’opérateur ++, les choses sont différentes.

La fonction Operator++(int) doit créer une copie.En effet, postfix ++ devrait renvoyer une valeur différente de celle qu'il contient :il doit conserver sa valeur dans une variable temporaire, incrémenter sa valeur et renvoyer la température.Dans le cas de Operator++(), préfixe ++, il n'est pas nécessaire de créer une copie :l'objet peut s'incrémenter puis simplement se retourner.

Voici une illustration du propos :

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
}

Chaque fois que vous appelez Operator++(int), vous devez créer une copie et le compilateur ne peut rien y faire.Lorsque vous avez le choix, utilisez Operator++();de cette façon, vous n'enregistrez pas de copie.Cela peut être significatif dans le cas de nombreux incréments (grande boucle ?) et/ou d'objets volumineux.

Voici un point de référence pour le cas où les opérateurs d'incrémentation se trouvent dans des unités de traduction différentes.Compilateur avec g++ 4.5.

Ignorez les problèmes de style pour l'instant

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

O(n) incrément

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;
}

Résultats

Résultats (les délais sont en secondes) avec g++ 4.5 sur une machine virtuelle :

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

Incrément O(1)

Test

Prenons maintenant le fichier suivant :

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

Cela ne fait rien dans l'incrémentation.Cela simule le cas où l'incrémentation a une complexité constante.

Résultats

Les résultats varient désormais énormément :

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

Conclusion

En termes de performances

Si vous n'avez pas besoin de la valeur précédente, prenez l'habitude d'utiliser le pré-incrémentation.Soyez cohérent même avec les types intégrés, vous vous y habituerez et ne risquerez pas de subir une perte de performances inutile si jamais vous remplacez un type intégré par un type personnalisé.

Sur le plan sémantique

  • i++ dit increment i, I am interested in the previous value, though.
  • ++i dit increment i, I am interested in the current value ou increment i, no interest in the previous value.Encore une fois, vous vous y habituerez, même si ce n'est pas le cas maintenant.

Knuth.

L'optimisation prématurée est la racine de tout Mal.Tout comme une pessimisation prématurée.

Il n'est pas tout à fait exact de dire que le compilateur ne peut pas optimiser la copie des variables temporaires dans le cas postfix.Un test rapide avec VC montre qu'il peut au moins le faire dans certains cas.

Dans l'exemple suivant, le code généré est identique pour le préfixe et le suffixe, par exemple :

#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());
}

Que vous fassiez ++testFoo ou testFoo++, vous obtiendrez toujours le même code résultant.En fait, sans lire le décompte de l'utilisateur, l'optimiseur a réduit le tout à une constante.Donc ça:

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

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

Il en est résulté ce qui suit :

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

Ainsi, même s'il est certain que la version de Postfix pourrait être plus lente, il se pourrait bien que l'optimiseur soit assez performant pour se débarrasser de la copie temporaire si vous ne l'utilisez pas.

Le Guide de style Google C++ dit:

Préincrémentation et prédécrémentation

Utilisez le formulaire de préfixe (++ I) des opérateurs d'incrément et de décréments avec des itérateurs et d'autres objets de modèle.

Définition: Lorsqu'une variable est incrémentée (++ I ou I ++) ou décrémentée (--i ou i--) et que la valeur de l'expression n'est pas utilisée, il faut décider de préincéder (décrément) ou de post-encrément (décrément).

Avantages: Lorsque la valeur de retour est ignorée, le formulaire "pré" (++ i) n'est jamais moins efficace que le formulaire "Post" (i ++), et est souvent plus efficace.En effet, après l'incrément (ou décrément), il faut faire une copie de I, qui est la valeur de l'expression.Si je suis un itérateur ou un autre type non échec, la copie, je pourrais être coûteuse.Étant donné que les deux types d'incrément se comportent de la même manière lorsque la valeur est ignorée, pourquoi ne pas toujours pré-incrémentation?

Les inconvénients: La tradition s'est développée, en C, d'utiliser après l'incrément lorsque la valeur d'expression n'est pas utilisée, en particulier dans les boucles.Certains trouvent plus facile à lire, car le "sujet" (i) précède le "verbe" (++), tout comme en anglais.

Décision: Pour les valeurs scalaires simples (non-objet), il n'y a aucune raison de préférer une forme et nous le permettons non plus.Pour les itérateurs et autres types de modèles, utilisez le pré-incitation.

Je voudrais souligner un excellent article d'Andrew Koenig sur Code Talk très récemment.

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

Dans notre entreprise, nous utilisons également la convention ++iter pour la cohérence et les performances, le cas échéant.Mais Andrew soulève des détails négligés concernant l’intention par rapport aux performances.Il y a des moments où nous souhaitons utiliser iter++ au lieu de ++iter.

Donc, décidez d'abord de votre intention et si pre ou post n'a pas d'importance, optez pour pre car cela aura un certain avantage en termes de performances en évitant la création d'un objet supplémentaire et en le lançant.

@Kétan

... soulève des détails négligés concernant l'intention par rapport aux performances.Il y a des moments où nous souhaitons utiliser iter++ au lieu de ++iter.

Évidemment, post et pré-incrémentation ont une sémantique différente et je suis sûr que tout le monde convient que lorsque le résultat est utilisé, vous devez utiliser l'opérateur approprié.Je pense que la question est de savoir que faire lorsque le résultat est rejeté (comme dans for boucles).La réponse à ce La question (à mon humble avis) est que, puisque les considérations de performances sont au mieux négligeables, vous devriez faire ce qui est plus naturel.Pour moi-même ++i est plus naturel mais mon expérience me dit que je suis en minorité et que j'utilise i++ entraînera moins de frais généraux de métal pour la plupart les gens qui lisent votre code.

Après tout, c'est la raison pour laquelle la langue ne s'appelle pas "++C".[*]

[*] Insérer une discussion obligatoire sur ++C étant un nom plus logique.

Marque:Je voulais juste souligner que les opérateurs++ sont de bons candidats à intégrer, et si le compilateur choisit de le faire, la copie redondante sera éliminée dans la plupart des cas.(par exemple.types de POD, que sont généralement les itérateurs.)

Cela dit, il est toujours préférable d'utiliser ++iter dans la plupart des cas.:-)

La différence de performances entre ++i et i++ sera plus évident lorsque vous considérerez les opérateurs comme des fonctions de retour de valeur et comment ils sont implémentés.Pour faciliter la compréhension de ce qui se passe, les exemples de code suivants utiliseront int comme si c'était un struct.

++i incrémente la variable, alors renvoie le résultat.Cela peut être effectué sur place et avec un temps CPU minimal, ne nécessitant qu'une seule ligne de code dans de nombreux cas :

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

Mais on ne peut pas en dire autant de i++.

Post-incrémentation, i++, est souvent considéré comme renvoyant la valeur d'origine avant incrémentant.Cependant, une fonction ne peut renvoyer un résultat que lorsqu'elle est terminée.De ce fait, il devient nécessaire de créer une copie de la variable contenant la valeur d'origine, d'incrémenter la variable, puis de renvoyer la copie contenant la valeur d'origine :

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

Lorsqu'il n'y a pas de différence fonctionnelle entre le pré-incrémentation et le post-incrémentation, le compilateur peut effectuer une optimisation de telle sorte qu'il n'y ait aucune différence de performances entre les deux.Cependant, si un type de données composite tel qu'un struct ou class est impliqué, le constructeur de copie sera appelé en post-incrémentation, et il ne sera pas possible d'effectuer cette optimisation si une copie complète est nécessaire.En tant que tel, la pré-incrémentation est généralement plus rapide et nécessite moins de mémoire que la post-incrémentation.

  1. ++je - plus rapide n'utilise pas la valeur de retour
  2. je++ - plus rapide en utilisant la valeur de retour

Quand n'utilise pas la valeur de retour, le compilateur est assuré de ne pas utiliser de valeur temporaire dans le cas de ++je.Il n'est pas garanti d'être plus rapide, mais il est garanti de ne pas être plus lent.

Quand en utilisant la valeur de retour je++ Permet au processeur de pousser à la fois l'incrément et le côté gauche dans le pipeline car il ne dépend pas les uns des autres.++Je peux bloquer le pipeline car le processeur ne peut pas démarrer le côté gauche tant que l'opération de pré-incrémentation n'a pas parcouru tout son chemin.Encore une fois, un blocage du pipeline n'est pas garanti, car le processeur peut trouver d'autres éléments utiles à intégrer.

Et la raison pour laquelle vous devriez utiliser ++i même sur les types intégrés où il n'y a aucun avantage en termes de performances est de créer une bonne habitude pour vous-même.

@Marque:J'ai supprimé ma réponse précédente parce qu'elle était un peu flippante et méritait un vote négatif pour cela seul.Je pense en fait que c'est une bonne question dans le sens où elle demande ce qui préoccupe beaucoup de gens.

La réponse habituelle est que ++i est plus rapide que i++, et c'est sans aucun doute le cas, mais la plus grande question est "quand devriez-vous vous en soucier?"

Si la fraction du temps CPU consacrée à l’incrémentation des itérateurs est inférieure à 10 %, vous ne vous en soucierez peut-être pas.

Si la fraction du temps CPU consacrée à l'incrémentation des itérateurs est supérieure à 10 %, vous pouvez vérifier quelles instructions effectuent cette itération.Voyez si vous pouvez simplement incrémenter des entiers plutôt que d'utiliser des itérateurs.Il y a de fortes chances que vous le puissiez, et même si cela peut être dans un certain sens moins souhaitable, il y a de fortes chances que vous économisiez essentiellement tout le temps passé dans ces itérateurs.

J'ai vu un exemple où l'incrémentation de l'itérateur prenait bien plus de 90 % du temps.Dans ce cas, le passage à l'incrémentation d'entiers a réduit le temps d'exécution essentiellement de ce montant.(c'est à dire.mieux qu'une accélération 10x)

La question prévue portait sur le moment où le résultat n'est pas utilisé (cela ressort clairement de la question C).Quelqu'un peut-il résoudre ce problème puisque la question est « wiki communautaire » ?

A propos des optimisations prématurées, Knuth est souvent cité.C'est exact.mais Donald Knuth ne défendrait jamais ainsi l'horrible code que vous pouvez voir de nos jours.Avez-vous déjà vu a = b + c parmi les entiers Java (pas int) ?Cela équivaut à 3 conversions boxing/unboxing.Il est important d’éviter des choses comme ça.Et écrire inutilement i++ au lieu de ++i est la même erreur.MODIFIER:Comme Phresnel le dit joliment dans un commentaire, cela peut se résumer ainsi : « l'optimisation prématurée est un mal, tout comme la pessimisation prématurée ».

Même le fait que les gens soient plus habitués à i++ est un malheureux héritage du C, causé par une erreur conceptuelle de K&R (si vous suivez l'argument d'intention, c'est une conclusion logique ;et défendre K&R parce qu'ils sont K&R n'a aucun sens, ils sont géniaux, mais ils ne sont pas géniaux en tant que concepteurs de langage ;d'innombrables erreurs dans la conception C existent, allant de gets() à strcpy(), en passant par l'API strncpy() (elle aurait dû avoir l'API strlcpy() depuis le premier jour)).

Au fait, je fais partie de ceux qui ne sont pas assez habitués au C++ pour trouver ++i ennuyeux à lire.Pourtant, je l'utilise parce que je reconnais que c'est vrai.

@wilhelmtell

Le compilateur peut supprimer le temporaire.Verbatim de l'autre fil :

Le compilateur C++ est autorisé à éliminer les temporaires basés sur la pile même si cela modifie le comportement du programme.Lien MSDN pour VC 8 :

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

Il est temps de fournir aux gens des joyaux de sagesse ;) - il existe une astuce simple pour que l'incrément de postfix C++ se comporte à peu près de la même manière que l'incrément de préfixe (j'ai inventé cela pour moi-même, mais je l'ai également vu dans le code d'autres personnes, donc je ne suis pas seul).

Fondamentalement, l'astuce consiste à utiliser la classe d'assistance pour reporter l'incrémentation après le retour, et RAII vient à la rescousse.

#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;
}

Inventé est destiné à certains codes d'itérateurs personnalisés lourds, et il réduit le temps d'exécution.Le coût du préfixe par rapport au postfix est maintenant une référence, et s'il s'agit d'un opérateur personnalisé effectuant de gros déplacements, le préfixe et le postfix ont donné le même temps d'exécution pour moi.

Les deux sont aussi rapides;) Si vous le souhaitez, il s'agit du même calcul pour le processeur, c'est juste l'ordre dans lequel il est fait qui diffère.

Par exemple, le code suivant :

#include <stdio.h>

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

Produire l'assemblage suivant :

 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

Vous voyez que pour a++ et b++ c'est un mnémonique incl, donc c'est la même opération ;)

Quand tu écris i++ vous dites au compilateur d'incrémenter après avoir terminé cette ligne ou cette boucle.

++i est un peu différent de i++.Dans i++ vous incrémentez après avoir terminé la boucle mais ++i vous incrémentez directement avant la fin de la boucle.

++i est plus rapide que i++ car il ne renvoie pas une ancienne copie de la valeur.

C'est aussi plus intuitif :

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

Cet exemple C imprime "02" au lieu du "12" auquel vous pourriez vous attendre :

#include <stdio.h>

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

Idem pour le C++:

#include <iostream>
using namespace std;

int main(){
    int a = 0;
    cout << a++;
    cout << ++a;
    return 0;
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top