Y at-il une différence de performance entre i ++ et ++ i en C?

Y a-t-il une différence de performance entre i++ et ++i si la valeur résultante n’est pas utilisée?

Résumé exécutif: Non.

i++ pourrait potentiellement être plus lent que ++i , étant donné que l’ancienne valeur de i pourrait devoir être sauvegardée pour une utilisation ultérieure, mais en pratique, tous les compilateurs modernes vont l’optimiser.

Nous pouvons le démontrer en regardant le code de cette fonction, à la fois avec ++i et i++ .

 $ cat i++.c extern void g(int i); void f() { int i; for (i = 0; i < 100; i++) g(i); } 

Les fichiers sont les mêmes, sauf pour ++i et i++ :

 $ diff i++.c ++ic 6c6 < for (i = 0; i < 100; i++) --- > for (i = 0; i < 100; ++i) 

Nous allons les comstackr et obtenir l'assembleur généré:

 $ gcc -c i++.c ++ic $ gcc -S i++.c ++ic 

Et nous pouvons voir que les fichiers object et assembleur générés sont les mêmes.

 $ md5 i++.s ++is MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e MD5 (++is) = 90f620dda862cd0205cd5db1f2c8c06e $ md5 *.o MD5 (++io) = dd3ef1408d3a9e4287facccec53f7d22 MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22 

De l’ efficacité contre l’intention par Andrew Koenig:

Tout d’abord, il est loin d’être évident que ++i est plus efficace que i++ , du moins en ce qui concerne les variables entières.

Et :

Donc, la question que l’on devrait se poser n’est pas de savoir laquelle de ces deux opérations est la plus rapide, c’est laquelle de ces deux opérations exprime le mieux ce que vous essayez d’accomplir. Je soumets que si vous n’utilisez pas la valeur de l’expression, il n’y a jamais de raison d’utiliser i++ au lieu de ++i , car il n’y a jamais de raison de copier la valeur d’une variable, d’incrémenter la variable et de lancer la copier à distance.

Donc, si la valeur résultante n’est pas utilisée, j’utiliserais ++i . Mais pas parce que c’est plus efficace: parce qu’il indique correctement mon intention.

Une meilleure réponse est que ++i vais parfois être plus rapide mais jamais plus lent.

Tout le monde semble supposer que i un type intégré normal tel que int . Dans ce cas, il n’y aura pas de différence mesurable.

Cependant, si i un type complexe, vous pouvez trouver une différence mesurable. Pour i++ vous devez faire une copie de votre classe avant de l’incrémenter. En fonction de ce qui est impliqué dans une copie, il pourrait en effet être plus lent car avec ++it vous pouvez simplement renvoyer la valeur finale.

 Foo Foo::operator++() { Foo oldFoo = *this; // copy existing value - could be slow // yadda yadda, do increment return oldFoo; } 

Une autre différence est que, avec ++i vous avez la possibilité de renvoyer une référence au lieu d’une valeur. Encore une fois, cela peut être plus lent en fonction de la nature de la copie de votre object.

Un exemple concret où cela peut se produire serait l’utilisation d’iterators. Copier un iterator est peu susceptible de constituer un goulot d’étranglement dans votre application, mais il est toujours recommandé de prendre l’habitude d’utiliser ++i au lieu de i++ où le résultat n’est pas affecté.

Voici une observation supplémentaire si vous êtes préoccupé par la micro-optimisation. Les boucles de décrémentation peuvent être plus efficaces que les boucles d’incrémentation (en fonction de l’architecture du jeu d’instructions, par exemple ARM), à condition:

 for (i = 0; i < 100; i++) 

Sur chaque boucle, vous aurez chacune une instruction pour:

  1. Ajouter 1 à i .
  2. Comparez si i inférieur à 100 .
  3. Une twig conditionnelle si i est inférieure à 100 .

Alors qu'une boucle de décrémentation:

 for (i = 100; i != 0; i--) 

La boucle aura une instruction pour chacun des:

  1. Décrémenter i , en définissant l'indicateur d'état du registre du processeur.
  2. Une twig conditionnelle en fonction de l'état du registre de la CPU ( Z==0 ).

Bien sûr, cela ne fonctionne que lors de la décrémentation à zéro!

Rappelé du Guide du développeur du système ARM.

Prendre une feuille de Scott Meyers, Plus efficace c ++ Élément 6: Distinguer les formes de préfixe et de postfixe des opérations d’incrémentation et de décrémentation .

La version préfixe est toujours préférée au postfix en ce qui concerne les objects, en particulier en ce qui concerne les iterators.

La raison en est que si vous regardez la structure des appels des opérateurs.

 // Prefix Integer& Integer::operator++() { *this += 1; return *this; } // Postfix const Integer Integer::operator++(int) { Integer oldValue = *this; ++(*this); return oldValue; } 

En regardant cet exemple, il est facile de voir comment l’opérateur de préfixe sera toujours plus efficace que le postfixe. En raison de la nécessité d’un object temporaire dans l’utilisation du postfixe.

C’est pourquoi lorsque vous voyez des exemples utilisant des iterators, ils utilisent toujours la version préfixée.

Mais comme vous le soulignez pour int, il n’y a aucune différence en raison de l’optimisation du compilateur qui peut avoir lieu.

Réponse courte:

Il n’y a jamais de différence entre i++ et ++i en termes de vitesse. Un bon compilateur ne doit pas générer de code différent dans les deux cas.

Longue réponse:

Ce que toutes les autres réponses ne mentionnent pas, c’est que la différence entre ++i et i++ n’a de sens que dans l’expression trouvée.

Dans le cas de for(i=0; i , le i++ est seul dans sa propre expression: il y a un sharepoint séquence avant le i++ et il y en a un après. Ainsi, le seul code machine généré est "augmenter de 1 " et il est bien défini comment cela est séquencé par rapport au rest du programme. Donc, si vous voulez le changer en préfixe ++ , cela n’a pas d’importance, vous obtiendrez toujours le code machine "augmenter de 1 ".

Les différences entre ++i et i++ ne concernent que les expressions telles que array[i++] = x; contre array[++i] = x; . Certains peuvent argumenter et dire que le postfix sera plus lent dans de telles opérations car le registre où i réside doit être rechargé plus tard. Mais notez ensuite que le compilateur est libre de commander vos instructions comme bon lui semble, du moment qu'il ne "brise pas le comportement de la machine abstraite" comme le standard C l'appelle.

Donc, alors que vous pouvez supposer ce array[i++] = x; se traduit en code machine comme:

  • Stocke la valeur de i dans le registre A.
  • Adresse de stockage du tableau dans le registre B.
  • Ajouter A et B, stocker les résultats dans A.
  • A cette nouvelle adresse représentée par A, stockez la valeur de x.
  • Stocker la valeur de i dans le registre A // inefficace car des instructions supplémentaires ici, nous l'avons déjà fait une fois.
  • Incrémenter le registre A.
  • Enregistrez le registre A dans i .

le compilateur pourrait également produire le code plus efficacement, tel que:

  • Stocke la valeur de i dans le registre A.
  • Adresse de stockage du tableau dans le registre B.
  • Ajouter A et B, stocker les résultats dans B.
  • Incrémenter le registre A.
  • Enregistrez le registre A dans i .
  • ... // rest du code.

Tout simplement parce que vous, en tant que programmeur C, êtes formé pour penser que le postfix ++ se produit à la fin, le code machine ne doit pas être commandé de cette manière.

Donc, il n'y a pas de différence entre le préfixe et le postfix ++ dans C. Maintenant, en tant que programmeur C, vous devez varier, ce sont les personnes qui utilisent le préfixe de manière incohérente dans certains cas et postfixe dans d'autres cas. Cela suggère qu'ils ne savent pas comment C fonctionne ou qu'ils ont une connaissance incorrecte de la langue. C’est toujours un mauvais signe, cela suggère qu’ils prennent d’autres décisions discutables dans leur programme, basées sur la superstition ou les «dogmes religieux».

"Prefix ++ est toujours plus rapide" est en effet un de ces faux dogmes communs aux programmeurs potentiels.

S’il vous plaît, ne laissez pas la question de savoir «lequel est le plus rapide» comme le facteur décisif à utiliser. Les chances sont que vous ne vous soucierez jamais autant, et en outre, le temps de lecture du programmeur est beaucoup plus cher que le temps machine.

Utilisez ce qui convient le mieux à l’homme qui lit le code.

Tout d’abord: la différence entre i++ et ++i est impossible dans C.


Aux détails.

1. Le problème C ++ bien connu: ++i est plus rapide

En C ++, ++i est plus efficace si i ne i un object avec un opérateur d’incrément surchargé.

Pourquoi?
En ++i , l’object est d’abord incrémenté et peut ensuite être transmis en tant que référence const à toute autre fonction. Ce n’est pas possible si l’expression est foo(i++) car maintenant l’incrément doit être fait avant que foo() soit appelé, mais l’ancienne valeur doit être passée à foo() . Par conséquent, le compilateur est obligé de faire une copie de i avant d’exécuter l’opérateur d’incrémentation sur l’original. Les appels supplémentaires de constructeur / destructeur sont la partie incorrecte.

Comme indiqué ci-dessus, cela ne s’applique pas aux types fondamentaux.

2. Le fait peu connu: i++ peut être plus rapide

Si aucun constructeur / destructeur n’a besoin d’être appelé, ce qui est toujours le cas en C, ++i et i++ devraient être tout aussi rapides, non? Non, ils sont pratiquement aussi rapides, mais il peut y avoir de petites différences, ce que la plupart des autres intervenants ont mal compris.

Comment i++ peut-il être plus rapide?
Le point concerne les dépendances de données. Si la valeur doit être chargée de la mémoire, deux opérations ultérieures doivent être effectuées avec elle, en l’incrémentant et en l’utilisant. Avec ++i , l’incrémentation doit être effectuée avant que la valeur puisse être utilisée. Avec i++ , l’utilisation ne dépend pas de l’incrément, et le processeur peut effectuer l’opération d’utilisation parallèlement à l’opération d’incrémentation. La différence est au plus un cycle de processeur, donc il est vraiment impossible, mais il est là. Et c’est l’inverse que beaucoup pourraient attendre.

@Mark Même si le compilateur est autorisé à optimiser la copie temporaire (basée sur la stack) de la variable et que gcc (dans les versions récentes) le fait, cela ne signifie pas que tous les compilateurs le feront toujours.

Je viens de le tester avec les compilateurs que nous utilisons dans notre projet actuel et 3 sur 4 ne l’optimisent pas.

Ne supposez jamais que le compilateur a raison, surtout si le code éventuellement plus rapide, mais jamais plus lent, est aussi facile à lire.

Si vous n’avez pas une implémentation vraiment stupide de l’un des opérateurs de votre code:

Préférez toujours ++ i over i ++.

En C, le compilateur peut généralement les optimiser pour être les mêmes si le résultat est inutilisé.

Cependant, en C ++ si vous utilisez d’autres types qui fournissent leurs propres opérateurs ++, la version de préfixe est susceptible d’être plus rapide que la version postfixe. Donc, si vous n’avez pas besoin de la sémantique de postfix, il est préférable d’utiliser l’opérateur de préfixe.

Je peux penser à une situation où le postfixe est plus lent que l’incrémentation du préfixe:

Imaginez qu’un processeur avec le registre A soit utilisé comme accumulateur et que c’est le seul registre utilisé dans de nombreuses instructions (certains petits microcontrôleurs sont en réalité comme ça).

Imaginez maintenant le programme suivant et leur traduction en une assemblée hypothétique:

Incrément de préfixe:

 a = ++b + c; ; increment b LD A, [&b] INC A ST A, [&b] ; add with c ADD A, [&c] ; store in a ST A, [&a] 

Incrément Postfix:

 a = b++ + c; ; load b LD A, [&b] ; add with c ADD A, [&c] ; store in a ST A, [&a] ; increment b LD A, [&b] INC A ST A, [&b] 

Notez comment la valeur de b été forcée à être rechargée. Avec l’incrément de préfixe, le compilateur peut simplement incrémenter la valeur et continuer à l’utiliser, évitant éventuellement de le recharger car la valeur souhaitée est déjà dans le registre après l’incrément. Cependant, avec l’incrémentation postfixe, le compilateur doit gérer deux valeurs, l’une ancienne et l’autre la valeur incrémentée, ce qui, comme je l’ai montré ci-dessus, entraîne un access mémoire supplémentaire.

Bien sûr, si la valeur de l’incrément n’est pas utilisée, telle qu’un simple i++; statement, le compilateur peut (et fait) simplement générer une instruction d’incrémentation, indépendamment de l’utilisation du préfixe ou du postfixe.


En guise de note, je voudrais mentionner qu’une expression dans laquelle il y a un b++ ne peut pas simplement être convertie en une avec ++b sans effort supplémentaire (par exemple en ajoutant un - 1 ). Donc, comparer les deux si elles font partie d’une expression n’est pas vraiment valide. Souvent, lorsque vous utilisez b++ dans une expression, vous ne pouvez pas utiliser ++b , donc même si ++b était potentiellement plus efficace, ce serait tout simplement faux. L’exception est bien sûr si l’expression le demande (par exemple a = b++ + 1; qui peut être changé en a = ++b; ).

Je préfère toujours la pré-incrémentation, cependant …

Je voulais souligner que même dans le cas de l’appel de la fonction operator ++, le compilateur sera en mesure d’optimiser le temporaire si la fonction est intégrée. Puisque l’opérateur ++ est généralement court et souvent implémenté dans l’en-tête, il est probable qu’il soit intégré.

Donc, à des fins pratiques, il n’y a probablement pas beaucoup de différence entre les performances des deux formes. Cependant, je préfère toujours le pré-incrémentation car il semble préférable d’exprimer directement ce que j’essaie de dire, plutôt que de compter sur l’optimiseur pour le comprendre.

De plus, donner moins à l’optmiseur signifie probablement que le compilateur s’exécute plus rapidement.

Mon C est un peu rouillé, alors je m’excuse à l’avance. Rapide, je peux comprendre les résultats. Mais, je suis confus quant à la façon dont les deux fichiers sont sortis sur le même hachage MD5. Peut-être qu’une boucle for fonctionne de la même manière, mais les deux lignes de code suivantes ne génèrent-elles pas un assemblage différent?

 myArray[i++] = "hello"; 

contre

 myArray[++i] = "hello"; 

Le premier écrit la valeur dans le tableau, puis incrémente i. Le deuxième incrémente i écrit ensuite dans le tableau. Je ne suis pas expert en assemblage, mais je ne vois pas comment le même exécutable serait généré par ces deux lignes de code différentes.

Juste mes deux cents.