gcc -O optimisation: Aidez-moi à comprendre l'effet


4

Donc, j'apprends C et je suis en train de passer par les systèmes informatiques: Une perspective de programmeur 3e édition et les laboratoires associés. Je suis en train de faire le premier laboratoire pour lequel je dois implémenter (et donc mettre en place) la fonction suivante.

/* 
* fitsBits - return 1 if x can be represented as an 
* n-bit, two's complement integer. 
* 1 <= n <= 32 
* Examples: fitsBits(5,3) = 0, fitsBits(-4,3) = 1 
* Legal ops: ! ~ &^| + << >> 
* Max ops: 15 
* Rating: 2 
*/ 
int fitsBits(int x, int n) { 
    int sign_bit = (x >> 31 & 1); 
    int minus_one = ~1+1; 
    int n_minus_one = n + minus_one; 
    return (!(x >> n_minus_one) & !sign_bit) 
      | (!(~(x >> n_minus_one)) & sign_bit); 
} 

Cette fonction exécute _a_lot_ de testcases par rapport à la fonction de test suivante.

int test_fitsBits(int x, int n) 
{ 
    int TMin_n = -(1 << (n-1)); 
    int TMax_n = (1 << (n-1)) - 1; 
    return x >= TMin_n && x <= TMax_n; 
} 

Maintenant, voici où les choses bizarres qui se passe: par défaut, le code est compilé avec les indicateurs suivants -O -Wall -m32

mon code en cours contre la fonction de test produit l'affirmation suivante Fail: ERREUR: Test fitsBits (-2147483648 [0x80000000], 32 [0x20]) a échoué ... ... Donne 1 [0x1]. Devrait être 0 [0x0]

Il semble que mon code est correct et le code de test est faux. Après enquête, il semble que le test_function produit des résultats intermédiaires suivants:

> Tmin:-2147483648 
> TMax_n:2147483647 
> x: -2147483648 
> x >= TMin_n: 1 
> x <= TMax_n: 0 
> result: 0 

De toute évidence -2147483648 < = 2147483647, mais la comparaison produit en quelque sorte un 0.

Si je compile ce programme sans le drapeau -O, tous les tests réussissent. Quelqu'un peut-il faire la lumière sur ce comportement s'il vous plaît?

EDIT: Désolé Code de l'Assemblée est la mise en page horrible, ne savent pas exactement comment résoudre rapidement

Assembly Code without -O; 

.section __TEXT,__text,regular,pure_instructions 
.macosx_version_min 10, 11 
.globl _test_fitsBits 
.align 4, 0x90 
_test_fitsBits:       ## @test_fitsBits 
.cfi_startproc 
## BB#0: 
pushq %rbp 
Ltmp0: 
.cfi_def_cfa_offset 16 
Ltmp1: 
.cfi_offset %rbp, -16 
movq %rsp, %rbp 
Ltmp2: 
.cfi_def_cfa_register %rbp 
xorl %eax, %eax 
movb %al, %cl 
movl $1, %eax 
xorl %edx, %edx 
movl %edi, -4(%rbp) 
movl %esi, -8(%rbp) 
movl -8(%rbp), %esi 
subl $1, %esi 
movb %cl, -17(%rbp)   ## 1-byte Spill 
movl %esi, %ecx 
            ## kill: CL<def> ECX<kill> 
movl %eax, %esi 
shll %cl, %esi 
subl %esi, %edx 
movl %edx, -12(%rbp) 
movl -8(%rbp), %edx 
subl $1, %edx 
movl %edx, %ecx 
            ## kill: CL<def> ECX<kill> 
shll %cl, %eax 
subl $1, %eax 
movl %eax, -16(%rbp) 
movl -4(%rbp), %eax 
cmpl -12(%rbp), %eax 
movb -17(%rbp), %cl   ## 1-byte Reload 
movb %cl, -18(%rbp)   ## 1-byte Spill 
jl LBB0_2 
## BB#1: 
movl -4(%rbp), %eax 
cmpl -16(%rbp), %eax 
setle %cl 
movb %cl, -18(%rbp)   ## 1-byte Spill 
LBB0_2: 
movb -18(%rbp), %al   ## 1-byte Reload 
andb $1, %al 
movzbl %al, %eax 
popq %rbp 
retq 
.cfi_endproc 

.globl _main 
.align 4, 0x90 
_main:         ## @main 
.cfi_startproc 
## BB#0: 
pushq %rbp 
Ltmp3: 
.cfi_def_cfa_offset 16 
Ltmp4: 
.cfi_offset %rbp, -16 
movq %rsp, %rbp 
Ltmp5: 
.cfi_def_cfa_register %rbp 
xorl %eax, %eax 
movl $0, -4(%rbp) 
movl %edi, -8(%rbp) 
movq %rsi, -16(%rbp) 
popq %rbp 
retq 
.cfi_endproc 


.subsections_via_symbols 

Code de l'Assemblée avec -O:

.section __TEXT,__text,regular,pure_instructions 
.macosx_version_min 10, 11 
.globl _test_fitsBits 
.align 4, 0x90 
_test_fitsBits:       ## @test_fitsBits 
.cfi_startproc 
## BB#0: 
pushq %rbp 
Ltmp0: 
.cfi_def_cfa_offset 16 
Ltmp1: 
.cfi_offset %rbp, -16 
movq %rsp, %rbp 
Ltmp2: 
.cfi_def_cfa_register %rbp 
            ## kill: ESI<def> ESI<kill>  RSI<def> 
leal -1(%rsi), %ecx 
movl $1, %eax 
            ## kill: CL<def> CL<kill> ECX<kill> 
shll %cl, %eax 
movl %eax, %ecx 
negl %ecx 
cmpl %edi, %eax 
setg %al 
cmpl %ecx, %edi 
setge %cl 
andb %al, %cl 
movzbl %cl, %eax 
popq %rbp 
retq 
.cfi_endproc 

.globl _main 
.align 4, 0x90 
_main:         ## @main 
.cfi_startproc 
## BB#0: 
pushq %rbp 
Ltmp3: 
.cfi_def_cfa_offset 16 
Ltmp4: 
.cfi_offset %rbp, -16 
movq %rsp, %rbp 
Ltmp5: 
.cfi_def_cfa_register %rbp 
xorl %eax, %eax 
popq %rbp 
retq 
.cfi_endproc 


.subsections_via_symbols 
+2

Pouvez-vous utiliser '-S' gcc pour le code assembleur de sortie, puis après le code assembleur pour' test_fitsBits'? 23 nov.. 15

+1

Quelle est la largeur de 'int' sur votre plate-forme? Quelle est la plage de valeurs pour 'n'? Rappelez-vous que c'est un comportement indéfini pour avoir l'opérande droit de l'opérateur de décalage gauche négatif ou supérieur ou égal à la largeur de l'opérande gauche promu. 23 nov.. 15

+1

int est 32bit, mais je suis sur une plate-forme 64 bits. Ce sont les valeurs pour INT_MAX et INT_MIN lorsqu'imprimées à partir des limites.h: 2147483647 -2147483648. 23 nov.. 15

  0

@immibis: Code d'assemblage ajouté 23 nov.. 15

4

La réponse d'oauh explique pourquoi ce comportement n'est pas défini, mais pas pourquoi le problème n'apparaît qu'avec -O.

-O rend le compilateur plus difficile de trouver des choses à optimiser. Dans ce cas, il semble avoir réalisé que x <= (1 << (n-1)) - 1 est vraiment le même que x < (1 << (n-1)) - donc il a été en mesure de supprimer le - 1.

Cependant, ceci n'est équivalent à l'ancien code que si 1 << (n-1) ne déborde pas. Le compilateur fait quand même cette optimisation, parce que le résultat est autorisé à être faux si le dépassement se produit - c'est parce que n'importe quoi est autorisé si le débordement se produit, parce que c'est un comportement indéfini.


7
int TMin_n = -(1 << (n-1)); 
int TMax_n = (1 << (n-1)) - 1; 

Sur un système avec 32 bits int les deux expressions de décalage de bits ci-dessus invoquent un comportement indéfini lorsque n est 32. Lorsque int est 32 bits alors 1 << 31 est UB en C comme 1 << 31 n'est pas représentable dans un int.

  0

Merci pour votre réponse. J'avais vu l'avertissement du compilateur sur le débordement, mais je veux comprendre pourquoi l'indicateur -O a l'effet observé. 23 nov.. 15

+1

Il peut être utile de souligner que "comportement indéfini" signifie vraiment que * n'importe quoi * peut arriver. Donc, même si la valeur est imprimée correctement en un point, cela ne signifie pas que la valeur correcte est utilisée dans les calculs, etc. En ce qui concerne l'optimisation, tous les paris sont désactivés. L'optimiseur pourrait juste penser, "que diable, nous faisons des choses indéfinies ici, finissons juste aussi vite que possible". 23 nov.. 15

  0

Négatif INT_MIN ou soustraire 1 de lui sont également indéfinis, donc la fixation du décalage seul ne sera pas suffisante. 23 nov.. 15

  0

@ user2357112 sauf s'il le fixe à l'aide d'un type plus large, par exemple '1ULL << (n - 1)' et l'affectation à un 'long unsigned long'. 23 nov.. 15

+1

@ MathieuBorderé il s'agit d'un comportement non défini et le compilateur prend deux approches en fonction de l'option d'optimisation utilisée qui est silencieuse en cas de comportement indéfini. 23 nov.. 15

+1

@ MathieuBorderé fondamentalement sans option d'optimisation le compilateur est en mode -O0' et fait généralement un over-over, ajoutez '-fwrapv' (ceci force le bouclage sur le dépassement arithmétique signé) à' -O' et vous pouvez obtenir le même résultat comme avec '-O0' (ou aucune option d'optimisation). 23 nov.. 15

  0

Incidemment, qu'est-ce qu'une implémentation raisonnable de 'fitBits', en évitant un comportement indéfini et en couvrant toute la plage entière entière? Le meilleur que je puisse trouver est le 8-operation 'unsigned int k = 1U << n - 1; retour! ((x + k) & ~ (k << 1) + 1); '. 23 nov.. 15

  0

forcer envelopper autour de "fixe" le problème, merci pour la perspicacité @ouah 23 nov.. 15


1

As pointed out by @ouah, votre code utilise undefined behavior. Plutôt que de faire des erreurs discutables, C tend à dire que son comportement est «indéfini». Cela signifie que le compilateur peut faire ce qu'il veut.La plupart des compilateurs essaieront de faire ce que vous voulez dire, mais d'autres ont interprété le "comportement indéfini" de façon plus libérale et decided you really wanted to play a game of Rogue.

-O indique au compilateur de passer un peu de temps à essayer d'optimiser votre code. Cela modifie la façon dont le compilateur traduit votre code C en code machine. Il va faire moins d'hypothèses sur le code pour essayer de le transformer en une version plus efficace, mais équivalente. Tout comportement indéfini qui fonctionnait auparavant peut maintenant être rompu. Inversement, votre code peut fonctionner correctement avec -O et se casser lorsque vous l'éteignez. Ou -g pourrait le casser. Ou en exécutant votre code dans le débogueur. Ou en ajoutant un printf. Ou une brise raide.

Ces types de bogues qui apparaissent et disparaissent portent le nom heisenbugs après le principe d'incertitude de Heisenberg qui dit que l'observation d'un système modifie un système.

C'est pourquoi il est important d'activer tous les indicateurs d'avertissement de votre compilateur (-Wall n'est pas tout, clang a -Weverything) et les éliminer tous. Un autre outil qui aide à attraper ces sortes de problèmes de mémoire est Valgrind.


0

Modifier test.c fichier:

int test_fitsBits(int x, int n) { 
    int TMin_n, TMax_n; 
    if (n < 32) { 
     TMin_n = -(1 << (n-1)); 
     TMax_n = (1 << (n-1)) - 1; 
    } else { 
     TMin_n = 0x80000000; 
     TMax_n = ~TMin_n; 
    } 
     return x >= TMin_n && x <= TMax_n; 
}