Nous avons la question est-il une différence de performance entre i++
et ++i
en C ?
Quelle est la réponse pour C ++?
Nous avons la question est-il une différence de performance entre i++
et ++i
en C ?
Quelle est la réponse pour C ++?
Réponses:
[Résumé: à utiliser ++i
si vous n'avez pas de raison spécifique d'utiliseri++
.]
Pour C ++, la réponse est un peu plus compliquée.
Si i
est un type simple (pas une instance d'une classe C ++), alors la réponse donnée pour C ("Non, il n'y a pas de différence de performances") est valable, car le compilateur génère le code.
Cependant, si i
est une instance d'une classe C ++, alors i++
et ++i
effectuent des appels à l'une des operator++
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 une operator++
fonction, il n'y a aucun moyen d'optimiser la tmp
variable et son constructeur de copie associé. Si le constructeur de copie coûte cher, cela peut avoir un impact significatif sur les performances.
Oui. Il y a.
L'opérateur ++ peut être défini ou non comme une fonction. Pour les types primitifs (int, double, ...) les opérateurs sont intégrés, donc le compilateur sera probablement en mesure d'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 l'opérateur ++ (), préfixe ++, il n'est pas nécessaire de créer une copie: l'objet peut s'incrémenter puis se retourner simplement.
Voici une illustration du point:
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. Il peut être important dans le cas de nombreux incréments (grande boucle?) Et / ou de gros objets.
C t(*this); ++(*this); return t;
Sur la deuxième ligne, vous incrémentez le pointeur this à droite, alors comment est- t
il mis à jour si vous l'incrémentez. Les valeurs de cela n'étaient-elles pas déjà copiées t
?
The operator++(int) function must create a copy.
non, ça ne l'est pas. Pas plus de copies queoperator++()
Voici une référence pour le cas où les opérateurs d'incrémentation sont dans différentes unités de traduction. 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;
}
// 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 (les temps 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
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.
Les résultats varient désormais considérablement:
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
Si vous n'avez pas besoin de la valeur précédente, prenez l'habitude d'utiliser le pré-incrément. Soyez cohérent même avec les types intégrés, vous vous y habituerez et ne courrez pas le risque de subir des pertes de performances inutiles si vous remplacez un type intégré par un type personnalisé.
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 vous n'êtes pas en ce moment.L'optimisation prématurée est la racine de tout Mal. Tout comme la pessimisation prématurée.
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
, don't mind the actual tree structure (BSP, kd, Quadtree, Octree Grid, etc.). Une telle iterator aurait besoin de maintenir un certain état, par exemple parent node
, child node
, index
et d'autres choses comme ça. Dans l'ensemble, ma position est, même si seuls quelques exemples existent, ...
Il n'est pas tout à fait correct de dire que le compilateur ne peut pas optimiser la copie de variable temporaire dans le cas de 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 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());
Résultat:
00401000 push 0Ah
00401002 push offset string "Value: %d\n" (402104h)
00401007 call dword ptr [__imp__printf (4020A0h)]
Donc, bien qu'il soit certain que la version postfixe pourrait être plus lente, il se pourrait bien que l'optimiseur soit suffisamment bon pour se débarrasser de la copie temporaire si vous ne l'utilisez pas.
Le guide de style Google C ++ dit:
Préincrément et prédécrément
Utilisez le préfixe (++ i) des opérateurs d'incrémentation et de décrémentation 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é-incrémenter (décrémenter) ou de post-incrémenter (décrémenter).
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émentation (ou la décrémentation), une copie de i doit être effectuée, qui est la valeur de l'expression. Si i est un itérateur ou un autre type non scalaire, la copie de i pourrait ê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 simplement toujours pré-incrémenter?
Inconvénients: La tradition s'est développée, en C, d'utiliser le post-incrément lorsque la valeur d'expression n'est pas utilisée, en particulier dans les boucles for. Certains trouvent que le post-incrément est 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 autorisons l'une ou l'autre. Pour les itérateurs et autres types de modèles, utilisez la pré-incrémentation.
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 de ++ 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 vs la performance. Il y a des moments où nous voulons utiliser iter ++ au lieu de ++ iter.
Donc, décidez d'abord de votre intention et si pré ou post n'a pas d'importance, alors allez avec pre car cela aura un avantage en termes de performances en évitant de créer un objet supplémentaire et en le jetant.
@Ketan
... soulève des détails négligés concernant l'intention par rapport aux performances. Il y a des moments où nous voulons utiliser iter ++ au lieu de ++ iter.
De toute évidence, le post et le pré-incrément 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 ce qu'il faut faire lorsque le résultat est rejeté (comme dans les for
boucles). La réponse à cette question (à mon humble avis) est que, puisque les considérations de performances sont au mieux négligeables, vous devez faire ce qui est plus naturel. Pour moi, ++i
c'est plus naturel, mais mon expérience me dit que je suis en minorité et que l'utilisation i++
entraînera moins de surcharge métallique pour la plupart des gens qui lisent votre code.
Après tout, c'est la raison pour laquelle la langue n'est pas appelée " ++C
". [*]
[*] Insérer une discussion obligatoire sur le fait d' ++C
être un nom plus logique.
Lorsqu'il n'utilise pas la valeur de retour, le compilateur est garanti de ne pas utiliser de temporaire dans le cas de ++ i . Pas garanti pour être plus rapide, mais pas pour être plus lent.
Lorsque vous utilisez la valeur de retour, i ++ permet au processeur de pousser à la fois l'incrément et le côté gauche dans le pipeline car ils ne dépendent pas les uns des autres. ++ je peux bloquer le pipeline parce que le processeur ne peut pas démarrer le côté gauche jusqu'à ce que l'opération de pré-incrémentation ait serpenté tout au long. Encore une fois, un blocage de pipeline n'est pas garanti, car le processeur peut trouver d'autres choses utiles à coller.
Mark: Je voulais juste souligner que les ++ de l'opérateur sont de bons candidats pour être intégrés, et si le compilateur choisit de le faire, la copie redondante sera éliminée dans la plupart des cas. (Par exemple, les types de POD, qui 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 apparente lorsque vous considérerez les opérateurs comme des fonctions de retour de valeur et la façon dont ils sont implémentés. Pour faciliter la compréhension de ce qui se passe, les exemples de code suivants utiliseront int
comme s'il s'agissait d'un struct
.
++i
incrémente la variable, puis renvoie le résultat. Cela peut être fait sur place et avec un temps processeur 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 i++
.
Après l'incrémentation,, i++
est souvent considéré comme renvoyant la valeur d'origine avant l' incrémentation. Cependant, une fonction ne peut renvoyer un résultat que lorsqu'elle est terminée . Par conséquent, 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 pré-incrémentation et post-incrémentation, le compilateur peut effectuer une optimisation telle qu'il n'y ait pas de différence de performances entre les deux. Cependant, si un type de données composite tel que a 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, le pré-incrément est généralement plus rapide et nécessite moins de mémoire que le post-incrément.
@Mark: J'ai supprimé ma réponse précédente parce que c'était un peu flip, et méritais un downvote pour cela seul. En fait, je pense que c'est une bonne question dans le sens où elle demande ce que beaucoup de gens pensent.
La réponse habituelle est que ++ i est plus rapide que i ++, et c'est certainement le cas, mais la plus grande question est "quand devriez-vous vous en soucier?"
Si la fraction du temps processeur consacré à l'incrémentation des itérateurs est inférieure à 10%, cela ne vous dérange pas.
Si la fraction du temps processeur consacré à l'incrémentation des itérateurs est supérieure à 10%, vous pouvez voir 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 bien que cela puisse être dans un certain sens moins souhaitable, les chances sont plutôt bonnes que vous économiserez essentiellement tout le temps passé dans ces itérateurs.
J'ai vu un exemple où l'incrémentation d'itérateur consommait bien plus de 90% du temps. Dans ce cas, passer à l'incrémentation d'entier a réduit le temps d'exécution essentiellement de ce montant. (c'est-à-dire meilleur que 10x d'accélération)
@wilhelmtell
Le compilateur peut éluder le temporaire. Verbatim de l'autre thread:
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
Une raison pour laquelle vous devez utiliser ++ i même sur des 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.
Les deux sont aussi rapides;) Si vous voulez que ce soit le même calcul pour le processeur, c'est juste l'ordre dans lequel cela se fait qui diffère.
Par exemple, le code suivant:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
Produisez 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;)
La question voulue était de savoir quand le résultat n'est pas utilisé (cela ressort clairement de la question pour C). Quelqu'un peut-il résoudre ce problème puisque la question est "wiki communautaire"?
Concernant les optimisations prématurées, Knuth est souvent cité. C'est vrai. mais Donald Knuth ne défendrait jamais avec cela le code horrible 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 de boxe / unboxing. Il est important d'éviter des trucs comme ça. Et écrire inutilement i ++ au lieu de ++ i est la même erreur. EDIT: Comme phresnel le dit gentiment dans un commentaire, cela peut être résumé comme "l'optimisation prématurée est mauvaise, tout comme la pessimisation prématurée".
Même le fait que les gens soient plus habitués à i ++ est un héritage malheureux en C, causé par une erreur conceptuelle de K&R (si vous suivez l'argument de l'intention, c'est une conclusion logique; et défendre K&R parce qu'ils sont K&R n'a pas de sens, ils sont génial, mais ils ne sont pas excellents en tant que concepteurs de langage; d'innombrables erreurs dans la conception C existent, allant de gets () à strcpy (), à l'API strncpy () (il aurait dû avoir l'API strlcpy () depuis le premier jour) ).
Btw, je fais partie de ceux qui ne sont pas assez habitués au C ++ pour trouver ++ i ennuyeux à lire. Pourtant, je l'utilise car je reconnais que c'est juste.
++i
plus ennuyeux que i++
(en fait, je l'ai trouvé plus frais), mais le reste de votre message obtient ma pleine reconnaissance. Peut-être ajouter un point "l'optimisation prématurée est mauvaise, tout comme la pessimisation prématurée"
strncpy
servi un objectif dans les systèmes de fichiers qu'ils utilisaient à l'époque; le nom de fichier était un tampon de 8 caractères et il ne devait pas être terminé par null. Vous ne pouvez pas leur reprocher de ne pas voir 40 ans dans l'avenir de l'évolution du langage.
strlcpy()
était justifiée par le fait qu'elle n'avait pas encore été inventée.
Il est temps de fournir aux gens des joyaux de sagesse;) - il y a une astuce simple pour que l'incrémentation postfixe C ++ se comporte à peu près comme l'incrémentation du préfixe (j'ai inventé cela pour moi, mais je l'ai vu aussi dans le code d'autres personnes, donc je ne 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;
}
Invented est destiné à certains codes d'itérateurs personnalisés lourds et réduit le temps d'exécution. Le coût du préfixe par rapport au postfix est une référence maintenant, et s'il s'agit d'un opérateur personnalisé effectuant de lourds déplacements, le préfixe et le postfix ont produit le même temps d'exécution pour moi.
++i
est plus rapide que i++
parce qu'il ne retourne 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;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}