L'instruction ' if ' est-elle redondante avant les opérations modulo et avant assign?
Considérons le code suivant:
unsigned idx;
//.. some work with idx
if( idx >= idx_max )
idx %= idx_max;
Pourrait être simplifié à seulement la deuxième ligne:
idx %= idx_max;
Et atteindra le même résultat.
Plusieurs fois, j'ai rencontré le code suivant:
unsigned x;
//... some work with x
if( x!=0 )
x=0;
Pourrait être simplifié à
x=0;
Les questions:
- y a-t-il un sens à utiliser
if
et pourquoi? Surtout avec bras pouce jeu d'instructions. - ces
if
peuvent-ils être omises? - quelle optimisation fait le compilateur?
4 réponses
Si vous voulez comprendre ce que fait le compilateur, vous devrez simplement extraire un assemblage. Je recommande ce site (j'ai déjà entré le code de la question)): https://godbolt.org/g/FwZZOb .
Le premier exemple est plus intéressant.
int div(unsigned int num, unsigned int num2) {
if( num >= num2 ) return num % num2;
return num;
}
int div2(unsigned int num, unsigned int num2) {
return num % num2;
}
Génère:
div(unsigned int, unsigned int): # @div(unsigned int, unsigned int)
mov eax, edi
cmp eax, esi
jb .LBB0_2
xor edx, edx
div esi
mov eax, edx
.LBB0_2:
ret
div2(unsigned int, unsigned int): # @div2(unsigned int, unsigned int)
xor edx, edx
mov eax, edi
div esi
mov eax, edx
ret
Fondamentalement, le compilateur n'optimisera pas la branche, pour des raisons très spécifiques et logiques. Si la division entière était à peu près le même coût que la comparaison, alors la branche serait assez inutile. Mais la division entière (avec laquelle le module est généralement effectué) est en fait très coûteuse: http://www.agner.org/optimize/instruction_tables.pdf . les nombres varient considérablement selon l'architecture et la taille entière, mais il peut généralement s'agir d'une latence comprise entre 15 et près de 100 cycles.
En prenant une branche avant d'effectuer le module, vous pouvez vous épargner beaucoup de travail. Notez cependant: le compilateur ne transforme pas non plus le code sans branche dans une branche au niveau de l'assemblage. C'est parce que la branche a aussi un inconvénient: si le module finit par être nécessaire de toute façon, vous avez juste perdu un peu de temps.
Il n'y a aucun moyen de prendre une décision raisonnable sur l'optimisation correcte sans connaître la fréquence relative avec laquelle idx < idx_max
sera vrai. Ainsi, les compilateurs (gcc et clang font la même chose) choisissent de mapper le code d'une manière relativement transparente, laissant ce choix entre les mains du développeur.
Donc cette branche aurait pu être un choix très raisonnable.
La deuxième branche devrait être complètement inutile, car la comparaison et l'affectation sont de coût comparable. Cela dit, Vous pouvez voir dans le lien que les compilateurs n'effectueront toujours pas cette optimisation s'ils ont une référence à la variable. Si la valeur est une variable locale (comme dans votre code démontré), le compilateur optimisera la branche.
En somme le premier morceau de code peut-être une optimisation raisonnable, la seconde, sans doute fatigué programmeur.
Il existe un certain nombre de situations où l'écriture d'une variable avec une valeur qu'elle contient déjà peut être plus lente que la lecture, trouver la valeur désirée et sauter l'écriture. Certains systèmes ont un cache de processeur qui envoie toutes les demandes d'écriture à la mémoire immédiatement. Bien que de telles conceptions ne soient pas courantes aujourd'hui, elles étaient assez courantes car elles peuvent offrir une fraction substantielle de l'augmentation des performances que la mise en cache complète en lecture/écriture peut offrir, mais à une petite fraction de coût.
Le Code comme ci-dessus peut également être pertinent dans certaines situations multi-CPU. La situation la plus courante serait lorsque le code s'exécutant simultanément sur deux cœurs de processeur ou plus frappera à plusieurs reprises la variable. Dans un système de mise en cache multicœur avec un modèle de mémoire forte, un noyau qui veut écrire une variable doit d'abord négocier avec d'autres cœurs pour acquérir la propriété exclusive de la ligne de cache qui le contient, et doit ensuite négocier à nouveau pour abandonner ce contrôle le suivant temps tout autre noyau veut le lire ou l'écrire. De telles opérations sont susceptibles d'être très coûteuses, et les coûts devront être supportés même si chaque écriture stocke simplement la valeur du stockage déjà détenu. Si l'emplacement devient nul et n'est plus jamais écrit, cependant, les deux cœurs peuvent contenir la ligne de cache simultanément pour un accès en lecture seule non exclusif et ne jamais avoir à négocier davantage pour cela.
Dans presque toutes les situations où plusieurs processeurs pourraient frapper une variable, la variable devrait au minimum être déclaré volatile
. La seule exception, qui pourrait être applicable ici, serait dans les cas où toutes les Écritures dans une variable qui se produisent après le début de main()
stockeront la même valeur,et le code se comporterait correctement si un magasin par un CPU était visible dans un autre. Si faire une opération plusieurs fois serait inutile mais autrement inoffensif, et le but de la variable est de dire si cela doit être fait, alors de nombreuses implémentations peuvent être capables de générer meilleur code sans le qualificatif volatile
qu'avec, à condition qu'ils n'essaient pas d'améliorer l'efficacité en rendant l'écriture inconditionnelle.
Incidemment, si l'objet était accessible via un pointeur, il y en aurait un autre
raison possible du code ci-dessus: si une fonction est conçue pour accepter soit
un objet const
où un certain champ est nul, ou un objet non const
qui
devrait avoir ce champ mis à zéro, le code comme ci-dessus pourrait être nécessaire pour
assurer un comportement défini dans les deux cas.
Cordialement premier bloc de code: il s'agit d'une micro-optimisation basée sur les recommandations de Chandler Carruth pour Clang (voir ici pour plus d'informations), mais il ne tient pas nécessairement que ce serait une micro-optimisation valide sous cette forme (en utilisant if plutôt que ternary) ou sur un compilateur donné.
Modulo est une opération raisonnablement coûteuse, si le code est exécuté souvent et qu'il y a un fort maigre statistique d'un côté ou de l'autre du conditionnel, la branche du CPU la prédiction (étant donné un processeur moderne) réduira considérablement le coût de l'instruction de branche.
, Il semble une mauvaise idée d'utiliser le si là, pour moi.
Vous avez raison. Que ce soit idx >= idx_max
ou non, il sera sous idx_max après idx %= idx_max
. If idx < idx_max
, il sera inchangé, que le if soit suivi ou non.
Alors que vous pourriez penser que la ramification autour du modulo pourrait faire gagner du temps, le vrai coupable, je dirais, est que lorsque les branches sont suivies, les CPU modernes doivent réinitialiser leur pipeline, et cela coûte beaucoup de temps. Mieux vaut ne pas avoir à suivre une branche, que de faire un entier modulo, qui coûte à peu près autant de temps qu'une division entière.
EDIT: il s'avère que le module est assez lent par rapport à la branche, comme suggéré par d'autres ici. Voici un gars qui examine exactement la même question: Cppcon 2015: Chandler Carruth " Tuning C++: Benchmarks, et CPU, et compilateurs! Oh Mon!" (suggéré dans une autre question SO liée à dans une autre réponse à cette question).
Ce gars écrit des compilateurs, et pensait que ce serait plus rapide sans le branche; mais ses repères lui ont prouvé tort. Même lorsque la branche n'a été prise que 20% du temps, elle a été testée plus rapidement.
Une autre raison de ne pas avoir le if: une ligne de code de moins à maintenir, et pour quelqu'un d'autre de comprendre ce que cela signifie. Le gars dans le lien ci-dessus a effectivement créé une macro "module plus rapide". IMHO, ceci ou une fonction en ligne est la voie à suivre pour les applications critiques de performance, parce que votre code sera toujours beaucoup plus compréhensible sans la branche, mais exécuter aussi vite.
Enfin, le gars de la vidéo ci-dessus prévoit de faire connaître cette optimisation aux auteurs de compilateurs. Ainsi, le SI sera probablement ajouté pour vous, sinon dans le code. Par conséquent, juste le mod seul fera, quand cela se produit.