Éviter l'opérateur d'incrémentation Postfix

25

J'ai lu que je devrais éviter l'opérateur d'incrémentation postfix pour des raisons de performances (dans certains cas).

Mais cela n'affecte-t-il pas la lisibilité du code? À mon avis:

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

Ressemble mieux que:

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

Mais c'est probablement juste par habitude. Certes, je n'en ai pas vu beaucoup d'utilisation ++i.

Les performances sont-elles mauvaises pour sacrifier la lisibilité, dans ce cas? Ou suis-je simplement aveugle et ++iplus lisible que i++?

Mateen Ulhaq
la source
1
J'ai utilisé i++avant de savoir que cela pouvait affecter les performances ++i, alors j'ai changé. Au début, ce dernier avait l'air un peu étrange, mais après un certain temps, je m'y suis habitué et maintenant, c'est aussi naturel que i++.
gablin
15
++iet i++faire des choses différentes dans certains contextes, ne présumez pas qu'elles sont les mêmes.
Orbling
2
Est-ce à propos de C ou C ++? Ce sont deux langues très différentes! :-) En C ++, la boucle for idiomatique est for (type i = 0; i != 42; ++i). Non seulement peut operator++être surchargé, mais peut aussi operator!=et operator<. L'incrémentation du préfixe n'est pas plus cher que le postfix, pas égal n'est pas plus cher que moins que. Lesquels devrions-nous utiliser?
Bo Persson
7
Ne devrait-il pas être appelé ++ C?
Armand
21
@Stephen: C ++ signifie prendre C, y ajouter, puis utiliser l'ancien .
supercat

Réponses:

58

Les faits:

  1. i ++ et ++ i sont également faciles à lire. Vous ne l'aimez pas parce que vous n'y êtes pas habitué, mais il n'y a pratiquement rien de mal à l'interpréter, donc ce n'est plus un travail à lire ou à écrire.

  2. Dans au moins certains cas, l'opérateur postfix sera moins efficace.

  3. Cependant, dans 99,99% des cas, cela n'aura pas d'importance car (a) il agira de toute façon sur un type simple ou primitif et ce n'est un problème que s'il copie un gros objet (b) il ne sera pas dans une performance partie critique du code (c) vous ne savez pas si le compilateur l'optimisera ou non, il peut le faire.

  4. Ainsi, je suggère d'utiliser le préfixe à moins que vous n'ayez spécifiquement besoin que postfix soit une bonne habitude, juste parce que (a) c'est une bonne habitude d'être précis avec d'autres choses et (b) une fois dans une lune bleue, vous aurez l'intention d'utiliser postfix et faites-le dans le mauvais sens: si vous écrivez toujours ce que vous voulez dire, c'est moins probable. Il y a toujours un compromis entre performances et optimisation.

Vous devez utiliser votre bon sens et ne pas micro-optimiser jusqu'à ce que vous en ayez besoin, mais ne pas être manifestement inefficace pour le plaisir. Cela signifie généralement: premièrement, exclure toute construction de code qui est inefficace de manière inacceptable, même dans le code non critique en temps (normalement quelque chose représentant une erreur conceptuelle fondamentale, comme passer des objets de 500 Mo par valeur sans raison); et deuxièmement, de toutes les autres façons d'écrire le code, choisissez la plus claire.

Cependant, ici, je pense que la réponse est simple: je pense que l'écriture d'un préfixe, sauf si vous avez spécifiquement besoin de postfix, est (a) très légèrement plus claire et (b) très légèrement plus susceptible d'être plus efficace, vous devriez donc toujours l'écrire par défaut, mais ne vous en faites pas si vous oubliez.

Il y a six mois, je pensais comme vous, qu'i ++ était plus naturel, mais c'est purement ce à quoi vous êtes habitué.

EDIT 1: Scott Meyers, dans "C ++ plus efficace" auquel je fais généralement confiance, dit que vous devriez en général éviter d'utiliser l'opérateur postfix sur les types définis par l'utilisateur (car la seule implémentation sensée de la fonction d'incrémentation postfix est de faire un copie de l'objet, appelez la fonction d'incrémentation du préfixe pour effectuer l'incrémentation et renvoyez la copie, mais les opérations de copie peuvent être coûteuses).

Donc, nous ne savons pas s'il existe des règles générales sur (a) si cela est vrai aujourd'hui, (b) si cela s'applique également (moins) aux types intrinsèques (c) si vous devez utiliser "++" sur rien de plus qu'une classe d'itérateurs légers. Mais pour toutes les raisons que j'ai décrites ci-dessus, peu importe, faites ce que j'ai dit auparavant.

EDIT 2: Cela fait référence à la pratique générale. Si vous pensez que cela importe dans certains cas spécifiques, vous devez le profiler et le voir. Le profilage est facile et bon marché et fonctionne. Déduire des premiers principes ce qui doit être optimisé est difficile et coûteux et ne fonctionne pas.

Jack V.
la source
Votre annonce est juste sur l'argent. Dans les expressions où l'opérateur infix + et post-incrément ++ ont été surchargés, comme aClassInst = someOtherClassInst + yetAnotherClassInst ++, l'analyseur génèrera du code pour effectuer l'opération additive avant de générer le code pour effectuer l'opération post-incrémentation, allégeant ainsi le besoin de créer une copie temporaire. Le tueur de performances ici n'est pas post-incrément. C'est l'utilisation d'un opérateur d'infixe surchargé. Les opérateurs Infix produisent de nouvelles instances.
bit-twiddler
2
Je soupçonne fortement que la raison pour laquelle les gens sont «habitués» i++plutôt que ++ic'est à cause du nom d'un certain langage de programmation populaire référencé dans cette question / réponse ...
Shadow
61

Toujours coder le programmeur en premier et l'ordinateur en second.

S'il y a une différence de performances, une fois que le compilateur a jeté un œil expert sur votre code, ET vous pouvez le mesurer ET cela compte - alors vous pouvez le changer.

Martin Beckett
la source
7
SUPERBE déclaration !!!
Dave
8
@Martin: c'est exactement pourquoi j'utiliserais l'incrément de préfixe. La sémantique de Postfix implique de conserver l'ancienne valeur, et si elle n'est pas nécessaire, alors il est inexact de l'utiliser.
Matthieu M.
1
Pour un index de boucle qui serait plus clair - mais si vous itériez sur un tableau en incrémentant un pointeur et en utilisant un préfixe, cela signifiait commencer à une adresse illégale une avant le début qui serait mauvais indépendamment d'une amélioration des performances
Martin Beckett
5
@Matthew: Il n'est tout simplement pas vrai que le post-incrément implique de conserver une copie de l'ancienne valeur. On ne peut pas être sûr de la façon dont un compilateur gère les valeurs intermédiaires jusqu'à ce que l'on affiche sa sortie. Si vous prenez le temps de visualiser ma liste annotée de langage d'assemblage généré par GCC, vous verrez que GCC génère le même code machine pour les deux boucles. Cette absurdité de préférer le pré-incrément au post-incrément parce qu'il est plus efficace n'est guère plus qu'une conjecture.
bit-twiddler
2
@Mathhieu: Le code que j'ai publié a été généré avec l'optimisation désactivée. La spécification C ++ n'indique pas qu'un compilateur doit produire une instance temporaire d'une valeur lorsque la post-incrémentation est utilisée. Il indique simplement la priorité des opérateurs pré et post-incrémentation.
bit-twiddler
13

GCC produit le même code machine pour les deux boucles.

Code C

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

Code d'assemblage (avec mes commentaires)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols
bit-twiddler
la source
Et si l'optimisation était activée?
serv-inc
2
@user: Probablement aucun changement, mais vous attendez-vous vraiment à ce que bit-twiddler revienne bientôt?
Déduplicateur
2
Attention: alors qu'en C il n'y a pas de types définis par l'utilisateur avec des opérateurs surchargés, en C ++ il y en a, et la généralisation des types de base aux types définis par l'utilisateur n'est tout simplement pas valide .
Déduplicateur
@Deduplicator: Merci également d'avoir souligné que cette réponse ne se généralise pas aux types définis par l'utilisateur. Je n'avais pas regardé sa page utilisateur avant de demander.
serv-inc
12

Ne vous inquiétez pas des performances, disons 97% du temps. L'optimisation prématurée est la racine de tout Mal.

- Donald Knuth

Maintenant que cela est hors de notre chemin, faisons notre choix en toute sécurité :

  • ++i: incrément de préfixe , incrémente la valeur actuelle et donne le résultat
  • i++: incrément de suffixe , copie la valeur, incrémente la valeur actuelle, produit la copie

À moins qu'une copie de l'ancienne valeur ne soit requise, l'utilisation de l' incrémentation de suffixe est un moyen détourné de faire avancer les choses.

L'inexactitude vient de la paresse, utilisez toujours la construction qui exprime votre intention de la manière la plus directe, il y a moins de chances que le futur responsable puisse mal comprendre votre intention initiale.

Même si c'est (vraiment) mineur ici, il y a des moments où j'ai été vraiment perplexe en lisant le code: je me demandais vraiment si l'intention et l'express réel coïncidaient, et bien sûr, après quelques mois, ils (ou moi) ne se souvenait pas non plus ...

Donc, peu importe si cela vous convient ou non. Embrassez KISS . Dans quelques mois, vous aurez évité vos anciennes pratiques.

Matthieu M.
la source
4

En C ++, vous pourriez faire une différence de performances substantielle s'il y a des surcharges d'opérateurs impliquées, en particulier si vous écrivez du code basé sur des modèles et que vous ne savez pas quels itérateurs peuvent être transmis. La logique derrière tout itérateur X peut être à la fois substantielle et significative- c'est-à-dire lent et impossible à optimiser par le compilateur.

Mais ce n'est pas le cas en C, où vous savez que ce ne sera qu'un type trivial, et la différence de performances est triviale et le compilateur peut facilement s'optimiser.

Donc, un conseil: vous programmez en C, ou en C ++, et les questions concernent l'un ou l'autre, pas les deux.

DeadMG
la source
2

Les performances de l'une ou l'autre opération dépendent fortement de l'architecture sous-jacente. Il faut incrémenter une valeur qui est stockée en mémoire, ce qui signifie que le goulot d'étranglement de von Neumann est le facteur limitant dans les deux cas.

Dans le cas de ++ i, nous devons

Fetch i from memory 
Increment i
Store i back to memory
Use i

Dans le cas d'i ++, nous devons

Fetch i from memory
Use i
Increment i
Store i back to memory

Les opérateurs ++ et - remontent leur origine au jeu d'instructions PDP-11. Le PDP-11 pourrait effectuer un post-incrémentation automatique sur un registre. Il pourrait également effectuer une pré-décrémentation automatique sur une adresse effective contenue dans un registre. Dans les deux cas, le compilateur ne pouvait tirer parti de ces opérations au niveau de la machine que si la variable en question était une variable "registre".

bit-twiddler
la source
2

Si vous voulez savoir si quelque chose est lent, testez-le. Prenez un BigInteger ou équivalent, collez-le dans une boucle for similaire en utilisant les deux idiomes, assurez-vous que l'intérieur de la boucle n'est pas optimisé et chronométrez les deux.

Après avoir lu l'article, je ne le trouve pas très convaincant, pour trois raisons. Premièrement, le compilateur doit être en mesure d'optimiser la création d'un objet qui n'est jamais utilisé. Deuxièmement, le i++concept est idiomatique pour les boucles numériques , donc les cas que je peux voir réellement affectés sont limités à. Troisièmement, ils fournissent un argument purement théorique, sans aucun chiffre à l'appui.

Sur la base de la raison n ° 1 en particulier, je suppose que lorsque vous effectuez réellement le timing, ils seront juste à côté les uns des autres.

jprete
la source
-1

Tout d'abord, cela n'affecte pas la lisibilité de l'OMI. Ce n'est pas ce que vous aviez l'habitude de voir, mais il ne vous faudra que peu de temps pour vous y habituer.

Deuxièmement, à moins que vous n'utilisiez une tonne d'opérateurs de suffixe dans votre code, vous ne verrez probablement pas beaucoup de différence. L'argument principal pour ne pas les utiliser lorsque cela est possible est qu'une copie de la valeur de la var d'origine doit être conservée jusqu'à la fin des arguments où la var d'origine pouvait encore être utilisée. C'est soit 32bits soit 64bits selon l'architecture. Cela équivaut à 4 ou 8 octets ou 0,00390625 ou 0,0078125 Mo. Les chances sont très élevées qu'à moins que vous n'utilisiez une tonne d'entre elles qui doivent être enregistrées pendant une très longue période de temps, avec les ressources informatiques et la vitesse d'aujourd'hui, vous ne remarquerez même pas de différence en passant d'un postfixe à un préfixe.

EDIT: Oubliez cette partie restante car ma conclusion s'est avérée fausse (sauf pour la partie de ++ i et i ++ ne faisant pas toujours la même chose ... c'est toujours vrai).

Il a également été souligné plus tôt qu'ils ne font pas la même chose dans certains cas. Soyez prudent lorsque vous décidez de faire le changement. Je ne l'ai jamais essayé (j'ai toujours utilisé postfix) donc je ne sais pas avec certitude mais je pense que changer de postfix en préfixe donnera des résultats différents: (encore une fois je pourrais me tromper ... dépend du compilateur / interprète aussi)

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}
Kenneth
la source
4
L'opération d'incrémentation se produit à la fin de la boucle for, ils auraient donc exactement la même sortie. Cela ne dépend pas du compilateur / interprète.
jsternberg
@jsternberg ... Merci, je ne savais pas quand l'incrémentation s'est produite, car je n'ai jamais vraiment eu de raison de le tester. Ça fait trop longtemps que je n'ai pas fait de compilateurs à l'université! lol
Kenneth
Faux mauvais faux.
ruohola
-1

Je pense que sémantiquement, a ++iplus de sens que i++, donc je m'en tiendrai au premier, sauf qu'il est courant de ne pas le faire (comme en Java, où vous devriez l'utiliser i++parce qu'il est largement utilisé).

Oliver Weiler
la source
-2

Il ne s'agit pas uniquement de performances.

Parfois, vous voulez éviter d'implémenter la copie, car cela n'a pas de sens. Et puisque l'utilisation de l'incrément de préfixe ne dépend pas de cela, il est clairement plus simple de s'en tenir à la forme de préfixe.

Et utiliser différents incréments pour les types primitifs et les types complexes ... c'est vraiment illisible.

maxim1000
la source
-2

Sauf si vous en avez vraiment besoin, je m'en tiendrai à ++ i. Dans la plupart des cas, c'est ce que l'on entend. Ce n'est pas très souvent que vous avez besoin d'i ++, et vous devez toujours réfléchir à deux fois lors de la lecture d'une telle construction. Avec ++ i, c'est facile: vous ajoutez 1, vous l'utilisez, puis i est toujours le même.

Donc, je suis entièrement d'accord avec @martin beckett: rendez-vous plus facile, c'est déjà assez difficile.

Peter Frings
la source