C'est une optimisation courante que d'utiliser le déplacement conditionnel (Assembly cmov
) pour optimiser l'expression conditionnelle ?:
en C. Cependant, la norme C dit:
Le premier opérande est évalué; il existe un point de séquence entre son évaluation et l'évaluation du deuxième ou du troisième opérande (celui qui est évalué). Le deuxième opérande n'est évalué que si le premier est différent de 0; le troisième opérande n'est évalué que si le premier est égal à 0; le résultat est la valeur du deuxième ou du troisième opérande (celui qui est évalué), converti dans le type décrit ci-dessous. 110)
Par exemple, le code C suivant
#include <stdio.h>
int main() {
int a, b;
scanf("%d %d", &a, &b);
int c= a > b ? a + 1 : 2 + b;
printf("%d", c);
return 0;
}
générera le code asm associé optimisé comme suit:
call __isoc99_scanf
movl (%rsp), %esi
movl 4(%rsp), %ecx
movl $1, %edi
leal 2(%rcx), %eax
leal 1(%rsi), %edx
cmpl %ecx, %esi
movl $.LC1, %esi
cmovle %eax, %edx
xorl %eax, %eax
call __printf_chk
Selon la norme, l'expression conditionnelle n'aura qu'une seule branche évaluée. Mais ici, les deux branches sont évaluées, ce qui est contraire à la sémantique de la norme. Cette optimisation est-elle contre la norme C? Ou bien de nombreuses optimisations du compilateur ont-elles quelque chose d'incompatible avec la norme de langage?
L'optimisation est légale, en raison de la "règle comme si" , c'est-à-dire C11 5.1.2.3p6 .
Une implémentation conforme est simplement requise pour produire un programme qui, lors de son exécution, produit le même comportement observable que l'exécution du programme en utilisant la sémantique abstraite aurait produit . Le reste de la norme décrit simplement ces sémantiques abstraites .
Ce que le programme compilé fait en interne n'a pas d'importance du tout, la seule chose qui compte, c'est que lorsque le programme se termine, il n'a aucun autre comportement observable, sauf la lecture de a
et b
et l'impression la valeur de a + 1
ou b + 2
selon lequel a
ou b
est supérieur, à moins que quelque chose ne se produise et que le comportement ne soit défini. (Une entrée incorrecte entraîne la non initialisation de a, b et donc l'accès non défini; une erreur de plage et un débordement signé peuvent également se produire.) Si un comportement non défini se produit, tous les paris sont désactivés.
Puisque les accès aux variables volatiles doivent être évalués strictement selon la sémantique abstraite, vous pouvez vous débarrasser du déplacement conditionnel en utilisant volatile
ici:
#include <stdio.h>
int main() {
volatile int a, b;
scanf("%d %d", &a, &b);
int c = a > b ? a + 1 : 2 + b;
printf("%d", c);
return 0;
}
compile en
call __isoc99_scanf@PLT
movl (%rsp), %edx
movl 4(%rsp), %eax
cmpl %eax, %edx
jg .L7
movl 4(%rsp), %edx
addl $2, %edx
.L3:
leaq .LC1(%rip), %rsi
xorl %eax, %eax
movl $1, %edi
call __printf_chk@PLT
[...]
.L7:
.cfi_restore_state
movl (%rsp), %edx
addl $1, %edx
jmp .L3
par mon GCC Ubuntu 7.2.0-8ubuntu3.2
La norme C décrit une machine abstraite exécutant du code C. Un compilateur est libre d'effectuer n'importe quelle optimisation tant que cette abstraction n'est pas violée, c'est-à-dire qu'un programme conforme ne peut pas faire la différence.