web-dev-qa-db-fra.com

L'instruction `if` est-elle redondante avant modulo et avant d'affecter des opérations?

Considérez 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 obtiendra 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é en

x=0;

Questions:

  • Est-il judicieux d'utiliser if et pourquoi? Surtout avec ARM Thumb instruction set.
  • Ces if peuvent-ils être omis?
  • Quelle optimisation fait le compilateur?
46
kyb

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 loin 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 effectué avec généralement) est en fait très coûteuse: http://www.agner.org/optimize/instruction_tables.pdf . Les nombres varient considérablement en fonction de l'architecture et de la taille entière, mais il peut généralement s'agir d'une latence de 15 à près de 100 cycles.

En prenant une branche avant d'effectuer le module, vous pouvez réellement vous épargner beaucoup de travail. Remarquez cependant: le compilateur ne transforme pas non plus le code sans branche en une branche au niveau de l'assembly. C'est parce que la branche a aussi un inconvénient: si le module finit par être nécessaire de toute façon, vous perdez juste un peu de temps.

Il n'y a aucun moyen de déterminer raisonnablement 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.

Cette succursale aurait donc 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 illustré), le compilateur optimisera la branche.

En somme, le premier morceau de code est peut-être une optimisation raisonnable, le second, probablement juste un programmeur fatigué.

66
Nir Friedman

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 sa lecture, la recherche de la valeur déjà détenue et le saut de l'écriture. Certains systèmes ont un cache de processeur qui envoie immédiatement toutes les demandes d'écriture en mémoire. 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 du coût.

Un code comme celui-ci 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 solide, un cœur qui souhaite é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 la contient, puis négocier à nouveau pour renoncer à ce contrôle la prochaine fois. 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 jamais réé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 plus jamais avoir à négocier pour cela.

Dans presque toutes les situations où plusieurs CPU peuvent frapper une variable, la variable doit au minimum être déclarée 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, que tout stockage par un processeur était visible dans un autre. Si effectuer une opération plusieurs fois serait inutile mais sinon inoffensif, et le but de la variable est de dire si cela doit être fait, alors de nombreuses implémentations peuvent être en mesure de générer un meilleur code sans le qualificatif volatile qu'avec , à condition qu'ils n'essaient pas d'améliorer l'efficacité en rendant l'écriture inconditionnelle.

Par ailleurs, si l'objet était accessible via un pointeur, il y aurait une autre raison possible pour le code ci-dessus: si une fonction est conçue pour accepter soit un objet const où un certain champ est zéro, soit un nonconst objet qui devrait avoir ce champ mis à zéro, un code comme celui-ci peut être nécessaire pour garantir un comportement défini dans les deux cas.

7
supercat

Observe le 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 considère pas nécessairement que ce serait une micro-optimisation valide sous cette forme (en utilisant if plutôt que ternaire) ou sur un compilateur donné.

Modulo est une opération raisonnablement coûteuse, si le code est exécuté souvent et qu'il existe un fort penchant statistique d'un côté ou de l'autre du conditionnel, la prédiction de branche du CPU (étant donné un CPU moderne) réduira considérablement le coût de l'instruction de branche .

2
metamorphosis

Cela me semble une mauvaise idée d'utiliser le if there, pour moi.

Vous avez raison. Que ce soit idx >= idx_max, il sera sous idx_max après idx %= idx_max. Si idx < idx_max, il restera inchangé, que le if soit suivi ou non.

Bien que vous puissiez penser que le branchement autour du modulo pourrait faire gagner du temps, le vrai coupable, je dirais, est que lorsque les branches sont suivies, les processeurs modernes doivent redéfinir leur pipeline, ce qui coûte beaucoup de temps. Mieux vaut ne pas avoir à suivre une branche, plutôt qu'un module entier, 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, and CPUs, and Compilers! Oh My!" (suggéré dans un autre SO = question liée à une autre réponse à cette question).

Ce gars écrit des compilateurs et pensait que ce serait plus rapide sans la branche; mais ses repères lui ont donné tort. Même lorsque la branche n'était prise que 20% du temps, elle testait plus vite.

Une autre raison de ne pas avoir le if: une ligne de code en moins à gérer et à quelqu'un d'autre de comprendre ce que cela signifie. Le gars dans le lien ci-dessus a en fait créé une macro "module plus rapide". À mon humble avis, cette fonction ou une fonction en ligne est la voie à suivre pour les applications critiques pour les performances, car votre code sera bien plus compréhensible sans la branche, mais s'exécutera aussi rapidement.

Enfin, le gars de la vidéo ci-dessus prévoit de faire connaître cette optimisation aux rédacteurs du compilateur. Ainsi, le if sera probablement ajouté pour vous, sinon dans le code. Par conséquent, seul le mod fera l'affaire, lorsque cela se produira.

1
CodeLurker