Considérez le code suivant ( p
est de type unsigned char*
et bitmap->width
est d'un type entier, qui est exactement inconnu et dépend de la version d'une bibliothèque externe que nous utilisons):
for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
Vaut-il la peine de l’optimiser [..]
Pourrait-il y avoir un cas où cela pourrait donner des résultats plus efficaces en écrivant:
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0; x < width; ++x)
{
*p++ = 0xAA;
*p++ = 0xBB;
*p++ = 0xCC;
}
... ou est-ce trivial pour le compilateur d'optimiser?
Que considérez-vous comme un «meilleur» code?
Note de l'éditeur (Ike): pour ceux qui s'interrogent sur le texte barré, la question originale, telle que formulée, était dangereusement proche d'un territoire hors sujet et était sur le point d'être fermée malgré des retours positifs. Ceux-ci ont été radiés. Cependant, veuillez ne pas punir les personnes qui ont répondu à ces sections frappées de la question.
la source
*p
est du même type quewidth
alors, ce n'est pas trivial à optimiser, car celap
pourrait pointerwidth
et le modifier à l'intérieur de la boucle.p
pointe vers la même mémoire quebitmap->width
. Par conséquent, je ne peux pas légalement optimiser le premier exemple au second.Réponses:
À première vue, je pensais que le compilateur pouvait générer un assemblage équivalent pour les deux versions avec des indicateurs d'optimisation activés. Quand je l'ai vérifié, j'ai été surpris de voir le résultat:
La source
unoptimized.cpp
remarque: ce code n'est pas destiné à être exécuté.
La source
optimized.cpp
remarque: ce code n'est pas destiné à être exécuté.
Compilation
$ g++ -s -O3 unoptimized.cpp
$ g++ -s -O3 optimized.cpp
Assemblée (unoptimized.s)
Assemblage (optimisé.s)
diff
L'assembly généré pour la version optimisée charge en fait (
lea
) lawidth
constante contrairement à la version non optimisée qui calcule lewidth
décalage à chaque itération (movq
).Quand j'aurai le temps, je publie finalement un point de repère à ce sujet. Bonne question.
la source
const unsigned
lieu de simplementunsigned
dans le cas non optimisé.main
pour tester une optimisation. Gcc le marque délibérément comme froid et désactive ainsi certaines optimisations. Je ne sais pas si c'est le cas ici, mais c'est une habitude importante à prendre.bitmap
s'agit d'un fichier global. La version non-CSEd utilise un opérande mémoire tocmp
, ce qui n'est pas un problème pour perf dans ce cas. S'il s'agissait d'un local, le compilateur pourrait supposer que d'autres pointeurs ne pouvaient pas "le savoir" et pointer dessus. Ce n'est pas une mauvaise idée de stocker des expressions impliquant des globaux dans des variables temporaires, tant que cela améliore (ou ne nuit pas) la lisibilité, ou si les performances sont critiques. À moins qu'il ne se passe beaucoup de choses, ces habitants peuvent généralement vivre dans des registres et ne jamais être renversés.Les informations de votre extrait de code sont en fait insuffisantes pour pouvoir le dire, et la seule chose à laquelle je peux penser est l'aliasing. De notre point de vue, il est assez clair que vous ne voulez pas
p
etbitmap
pointer vers le même emplacement en mémoire, mais le compilateur ne le sait pas et (parce qu'ilp
est de typechar*
) le compilateur doit faire fonctionner ce code même sip
et sebitmap
chevauchent.Cela signifie dans ce cas que si la boucle change
bitmap->width
via le pointeur,p
cela doit être vu lors de la relecturebitmap->width
ultérieure, ce qui signifie à son tour que le stocker dans une variable locale serait illégal.Cela étant dit, je pense que certains compilateurs généreront parfois deux versions du même code (j'ai vu des preuves circonstancielles de cela, mais je n'ai jamais directement cherché des informations sur ce que fait le compilateur dans ce cas), et vérifier rapidement si les pointeurs alias et exécutez le code le plus rapide s'il détermine que c'est correct.
Cela étant dit, je maintiens mon commentaire sur la simple mesure des performances des deux versions, mon argent est de ne pas voir de différence de performance cohérente entre les deux versions du code.
À mon avis, des questions comme celles-ci sont acceptables si votre objectif est d'en apprendre davantage sur les théories et les techniques d'optimisation du compilateur, mais c'est une perte de temps (une micro-optimisation inutile) si votre objectif final est de rendre le programme plus rapide.
la source
restrict
qualificatif ne serait-il pas la réponse au problème d'aliasing dans ce cas?restrict
c'est en grande partie aléatoire. MSVC est le seul compilateur que j'ai vu qui semble le faire correctement. ICC perd les informations d'alias lors des appels de fonction même s'ils sont en ligne. Et GCC n'obtient généralement aucun avantage à moins que vous ne déclariez chaque paramètre d'entrée commerestrict
(y compristhis
pour les fonctions membres).char
tous les types d'alias sont associés , donc si vous avez un char *, vous devez l'utiliserrestrict
sur tout. Ou si vous avez forcé les règles strictes d'alias de GCC avec-fno-strict-aliasing
alors tout est considéré comme un alias possible.restrict
sémantique semblable en C ++ est N4150 .Ok, les gars, donc j'ai mesuré, avec
GCC -O3
(en utilisant GCC 4.9 sur Linux x64).Il s'avère que la deuxième version est 54% plus rapide!
Donc, je suppose que l'aliasing est le truc, je n'y avais pas pensé.
[Éditer]
J'ai essayé à nouveau la première version avec tous les pointeurs définis avec
__restrict__
, et les résultats sont les mêmes. Bizarre .. Soit l'aliasing n'est pas le problème, soit, pour une raison quelconque, le compilateur ne l'optimise pas bien même avec__restrict__
.[Modifier 2]
Ok, je pense que j'ai été à peu près capable de prouver que l'aliasing est le problème. J'ai répété mon test d'origine, cette fois en utilisant un tableau plutôt qu'un pointeur:
Et mesuré (a dû utiliser "-mcmodel = large" pour le lier). Puis j'ai essayé:
Les résultats de la mesure étaient les mêmes - On dirait que le compilateur a pu l'optimiser par lui-même.
Ensuite j'ai essayé les codes originaux (avec un pointeur
p
), cette fois quandp
c'est de typestd::uint16_t*
. Encore une fois, les résultats étaient les mêmes - en raison d'un aliasing strict. Ensuite, j'ai essayé de construire avec "-fno-strict-aliasing", et j'ai encore vu une différence de temps.la source
D'autres réponses ont souligné que le fait de sortir l'opération du pointeur de la boucle peut changer le comportement défini en raison des règles d'aliasing qui permettent à char d'aliaser n'importe quoi et n'est donc pas une optimisation autorisée pour un compilateur même si dans la plupart des cas c'est évidemment correct pour un humain programmeur.
Ils ont également souligné que le hissage de l'opération hors de la boucle est généralement, mais pas toujours, une amélioration du point de vue des performances et est souvent un inconvénient du point de vue de la lisibilité.
Je tiens à souligner qu'il existe souvent une «troisième voie». Plutôt que de compter jusqu'au nombre d'itérations souhaité, vous pouvez effectuer un compte à rebours jusqu'à zéro. Cela signifie que le nombre d'itérations n'est nécessaire qu'une seule fois au début de la boucle, il n'a pas besoin d'être stocké après cela. Mieux encore au niveau de l'assembleur, cela élimine souvent le besoin d'une comparaison explicite car l'opération de décrémentation définira généralement des indicateurs indiquant si le compteur était égal à zéro avant (indicateur de retenue) et après (indicateur zéro) la décrémentation.
Notez que cette version de la boucle donne x valeurs dans la plage 1..width plutôt que dans la plage 0 .. (largeur-1). Cela n'a pas d'importance dans votre cas car vous n'utilisez en fait x pour rien, mais c'est quelque chose dont il faut être conscient. Si vous voulez une boucle de compte à rebours avec des valeurs x dans la plage 0 .. (largeur-1), vous pouvez le faire.
Vous pouvez également vous débarrasser des casts dans les exemples ci-dessus si vous le souhaitez sans vous soucier de son impact sur les règles de comparaison car tout ce que vous faites avec bitmap-> width est de l'assigner directement à une variable.
la source
x --> 0
, résultant en l'opérateur "downto". Assez drôle. PS Je ne considère pas que faire une variable pour la condition finale soit un négatif pour la lisibilité, cela peut en fait être le contraire.static_cast<unsigned>(bitmap->width)
et utiliser à lawidth
place dans la boucle est en fait une amélioration de la lisibilité, car il y a maintenant moins de choses à analyser par le lecteur par ligne. Les opinions des autres peuvent cependant différer.do { } while()
, car dans ASM vous créez des boucles avec une branche conditionnelle à la fin. Les bouclesfor(){}
et habituelleswhile(){}
nécessitent des instructions supplémentaires pour tester la condition de la boucle une fois avant la boucle, si le compilateur ne peut pas prouver qu'elle s'exécute toujours au moins une fois. Bien sûr, utilisezfor()
ouwhile()
quand il est utile de vérifier si la boucle doit même s'exécuter une fois, ou quand elle est plus lisible.La seule chose ici qui peut empêcher l'optimisation est la règle stricte d'aliasing . En bref :
L'exception s'applique également aux pointeurs
unsigned
etsigned
char
.C'est le cas dans votre code: vous modifiez
*p
viap
quel est ununsigned char*
, donc le compilateur doit supposer qu'il pourrait pointer versbitmap->width
. Par conséquent, la mise en cache debitmap->width
est une optimisation invalide. Ce comportement empêchant l'optimisation est illustré dans la réponse de YSC .Si et seulement si
p
pointé vers un nonchar
et un nondecltype(bitmap->width)
type, la mise en cache serait-elle une optimisation possible.la source
La question posée à l'origine:
Et ma réponse à cela (recueillir un bon mélange de votes à la hausse et à la baisse ..)
Malgré les votes négatifs (et voyant maintenant le problème d'aliasing), je suis toujours satisfait de cela comme une réponse valide. Si vous ne savez pas s'il vaut la peine d'optimiser quelque chose, ce n'est probablement pas le cas.
Une question assez différente, bien sûr, serait la suivante:
Tout d'abord, votre application ou bibliothèque doit-elle s'exécuter plus rapidement qu'elle ne le fait actuellement? L'utilisateur attend-il trop longtemps? Votre logiciel prévoit-il la météo d'hier plutôt que celle de demain?
Vous seul pouvez vraiment le dire, en fonction de ce à quoi sert votre logiciel et de ce que vos utilisateurs attendent.
En supposant que votre logiciel nécessite une optimisation, la prochaine chose à faire est de commencer à mesurer. Les profileurs vous diront où votre code passe son temps. Si votre fragment ne s'affiche pas comme un goulot d'étranglement, il vaut mieux le laisser seul. Les profileurs et autres outils de mesure vous diront également si vos modifications ont fait une différence. Il est possible de passer des heures à optimiser le code, seulement pour constater que vous n'avez fait aucune différence perceptible.
Si vous n'écrivez pas de code «optimisé», votre code doit être aussi clair, net et concis que possible. L'argument «l'optimisation prématurée est mauvais» n'est pas une excuse pour un code bâclé ou inefficace.
Le code optimisé sacrifie normalement certains des attributs ci-dessus pour les performances. Cela pourrait impliquer l'introduction de variables locales supplémentaires, avoir des objets avec une portée plus large que prévue ou même inverser l'ordre normal des boucles. Tous ces éléments peuvent être moins clairs ou concis, alors documentez le code (brièvement!) Pour expliquer pourquoi vous faites cela.
Mais souvent, avec du code «lent», ces micro-optimisations sont le dernier recours. Le premier endroit à regarder est les algorithmes et les structures de données. Existe-t-il un moyen d'éviter de faire le travail? Les recherches linéaires peuvent-elles être remplacées par des recherches binaires? Une liste chaînée serait-elle plus rapide ici qu'un vecteur? Ou une table de hachage? Puis-je mettre en cache les résultats? Prendre de bonnes décisions «efficaces» ici peut souvent affecter les performances d'un ordre de grandeur ou plus!
la source
J'utilise le modèle suivant dans une situation comme celle-ci. Il est presque aussi court que le premier cas du vôtre, et est meilleur que le second cas, car il maintient la variable temporaire locale à la boucle.
Ce sera plus rapide avec un compilateur moins intelligent, une version de débogage ou certains indicateurs de compilation.
Edit1 : Placer une opération constante en dehors d'une boucle est un bon modèle de programmation. Il montre une compréhension des bases du fonctionnement de la machine, en particulier en C / C ++. Je dirais que l'effort pour faire ses preuves devrait être sur les personnes qui ne suivent pas cette pratique. Si le compilateur punit pour un bon modèle, c'est un bogue dans le compilateur.
Edit2:: J'ai mesuré ma suggestion par rapport au code d'origine sur vs2013, j'ai obtenu une amélioration de% 1. Pouvons-nous faire mieux? Une simple optimisation manuelle permet d'améliorer 3 fois la boucle d'origine sur une machine x64 sans recourir à des instructions exotiques. Le code ci-dessous suppose un système little endian et une image bitmap correctement alignée. TEST 0 est original (9 sec), TEST 1 est plus rapide (3 sec). Je parie que quelqu'un pourrait rendre cela encore plus rapide, et le résultat du test dépendrait de la taille du bitmap. Très bientôt dans le futur, le compilateur sera en mesure de produire du code toujours le plus rapide. J'ai peur que ce soit le futur lorsque le compilateur sera aussi une IA de programmeur, donc nous serions sans travail. Mais pour l'instant, écrivez simplement du code qui montre que vous savez qu'une opération supplémentaire dans la boucle n'est pas nécessaire.
la source
Il y a deux choses à considérer.
A) À quelle fréquence l'optimisation sera-t-elle exécutée?
Si la réponse n'est pas très fréquente, comme uniquement lorsqu'un utilisateur clique sur un bouton, ne vous inquiétez pas si cela rend votre code illisible. Si la réponse est 1000 fois par seconde, vous voudrez probablement opter pour l'optimisation. Si c'est même un peu complexe, assurez-vous de mettre un commentaire pour expliquer ce qui se passe pour aider le prochain qui arrive.
B) Cela rendra-t-il le code plus difficile à entretenir / dépanner?
Si vous ne voyez pas un énorme gain de performances, rendre votre code cryptique simplement pour enregistrer quelques cycles d'horloge n'est pas une bonne idée. Beaucoup de gens vous diront que tout bon programmeur devrait être capable de regarder le code et de comprendre ce qui se passe. C'est vrai. Le problème, c'est que dans le monde des affaires, le temps supplémentaire pour déterminer cela coûte de l'argent. Donc, si vous pouvez le rendre plus joli à lire, faites-le. Vos amis vous en remercieront.
Cela dit, j'utiliserais personnellement l'exemple B.
la source
Le compilateur est capable d'optimiser beaucoup de choses. Pour votre exemple, vous devriez opter pour la lisibilité, la maintenabilité et ce qui suit votre norme de code. Pour plus d'informations sur ce qui peut être optimisé (avec GCC), consultez ce billet de blog .
la source
En règle générale, laissez le compilateur faire l'optimisation pour vous, jusqu'à ce que vous décidiez que vous devez prendre le relais. La logique de cela n'a rien à voir avec les performances, mais plutôt avec la lisibilité humaine. Dans la grande majorité des cas, la lisibilité de votre programme est plus importante que ses performances. Vous devez viser à écrire un code plus facile à lire pour un humain, puis ne vous soucier de l'optimisation que lorsque vous êtes convaincu que les performances sont plus importantes que la maintenabilité de votre code.
Une fois que vous voyez que les performances sont importantes, vous devez exécuter un profileur sur le code pour déterminer quelles boucles sont inefficaces et les optimiser individuellement. Il peut en effet y avoir des cas où vous souhaitez faire cette optimisation (surtout si vous migrez vers C ++, où les conteneurs STL interviennent), mais le coût en termes de lisibilité est élevé.
De plus, je peux penser à des situations pathologiques où cela pourrait en fait ralentir le code. Par exemple, considérons le cas où le compilateur n'a pas pu prouver que
bitmap->width
c'était constant tout au long du processus. En ajoutant lawidth
variable, vous forcez le compilateur à maintenir une variable locale dans cette portée. Si, pour une raison spécifique à la plate-forme, cette variable supplémentaire empêchait une certaine optimisation de l'espace de pile, il se peut qu'elle doive réorganiser la façon dont elle émet des codes octets et produire quelque chose de moins efficace.A titre d'exemple, sous Windows x64, on est obligé d'appeler un appel API spécial,
__chkstk
dans le préambule de la fonction si la fonction utilisera plus d'une page de variables locales. Cette fonction donne aux fenêtres une chance de gérer les pages de garde qu'elles utilisent pour étendre la pile en cas de besoin. Si votre variable supplémentaire pousse l'utilisation de la pile de moins de 1 page à au-dessus de 1 page, votre fonction est maintenant obligée d'appeler__chkstk
chaque fois qu'elle est entrée. Si vous optimisez cette boucle sur un chemin lent, vous risquez en fait de ralentir le chemin rapide plus que ce que vous avez enregistré sur le chemin lent!Bien sûr, c'est un peu pathologique, mais le point de cet exemple est que vous pouvez réellement ralentir le compilateur. Cela montre simplement que vous devez profiler votre travail pour déterminer où vont les optimisations. En attendant, veuillez ne sacrifier en aucune manière la lisibilité pour une optimisation qui puisse ou non avoir une importance.
la source
La comparaison est erronée car les deux extraits de code
et
ne sont pas équivalents
Dans le premier cas, il
width
est dépendant et non constant, et on ne peut pas supposer qu'il ne peut pas changer entre les itérations suivantes. Ainsi, il ne peut pas être optimisé, mais doit être vérifié à chaque boucle .Dans votre cas optimisé, une variable locale reçoit la valeur de
bitmap->width
à un moment donné pendant l'exécution du programme. Le compilateur peut vérifier que cela ne change pas réellement.Avez-vous pensé au multi-threading, ou peut-être que la valeur pourrait être dépendante de l'extérieur de sorte que sa valeur soit volatile. Comment s'attendrait-on à ce que le compilateur trouve toutes ces choses si vous ne le dites pas?
Le compilateur ne peut faire que ce que votre code le permet.
la source
À moins que vous ne sachiez exactement comment le compilateur optimise le code, il est préférable de faire vos propres optimisations en conservant la lisibilité et la conception du code. En pratique, il est difficile de vérifier le code d'assemblage pour chaque fonction que nous écrivons pour les nouvelles versions du compilateur.
la source
Le compilateur ne peut pas optimiser
bitmap->width
car la valeur dewidth
peut être modifiée entre les itérations. Il y a quelques raisons les plus courantes:iterator::end()
oucontainer::size()
il est donc difficile de prédire si elle retourne toujours le même résultat.Pour résumer (mon opinion personnelle) pour les endroits qui nécessitent un haut niveau d'optimisation, vous devez le faire vous-même, dans d'autres endroits, laissez-le, le compilateur peut l'optimiser ou non, s'il n'y a pas de grande différence, la lisibilité du code est la cible principale.
la source