Se connecter avec
S'enregistrer | Connectez-vous

Optimisation gcc

Dernière réponse : dans Programmation

Bonjour,

Je n'arrive pas à comprendre en quoi le fait de compiler le code suivant avec -O2 puisse modifier sont comportement :

  1. #include <stdio.h>
  2.  
  3. int main(void){
  4. signed short int b=1;
  5. printf("%d\n",sizeof(b));
  6. while(++b>0);
  7. return 0;
  8. }


Avec -O1, le programme fait ce qu'on attend de lui, cad quand b atteins 32767+1, la boucle s'arrête. Avec -O2, b continue de croître au delà de la valeur max d'un short int..

Le code suivant donne le même résultat avec -O1 et -O2 :

  1. #include <stdio.h>
  2.  
  3. int main(void){
  4. signed short int b=32767;
  5. printf("%d\n",++b);
  6. return 0;
  7. }


Il doit certainement s'agir d'une subtilité documentée quelque part, mais je n'est pas trouvé.
Merci d'avance.

Autres pages sur : optimisation gcc

Lassé par la pub ? Créez un compte

le plus "simple" est de voir le code assembleur généré par la compilation de ces deux programmes avec la commande : gcc -O2 -S -c foo.c

La mis à part les init qui peuvent paraitre barbare, tu pourras te rendre compte du nombre de lignes réellement utilisés ainsi que des opérations faites.

Bon ben d'après le code assembleur, on à bien, avec -O2, une boucle infinie (ligne 18 et 19):

  1. .file "test.c"
  2. .section .rodata.str1.1,"aMS",@progbits,1
  3. .LC0:
  4. .string "%d\n"
  5. .text
  6. .p2align 4,,15
  7. .globl main
  8. .type main, @function
  9. main:
  10. pushl %ebp
  11. movl $2, %eax
  12. movl %esp, %ebp
  13. andl $-16, %esp
  14. subl $16, %esp
  15. movl %eax, 4(%esp)
  16. movl $.LC0, (%esp)
  17. call printf
  18. .L2:
  19. jmp .L2
  20. .size main, .-main
  21. .ident "GCC: (GNU) 4.4.2"
  22. .section .note.GNU-stack,"",@progbits

Oui mais bon, en logique, un signed int doit changer de signe après une certaine valeur. :sweat: 
Bref se méfier des optimisations... Il vaut mieux être explicite et faire un peut de zèle. En tout cas, ce n'est pas du tout le comportement que j'attends des optimisations. Je ne vais pas mètre -O2 pour compiler tout mes soft depuis les sources. :pfff: 


En tout cas, merci .
Expert Programmation

Attention, tantal, entre ce que tu considères être de l'ordre du normal, de tes propres optimisations et de ce que tu attends du compilateur.

Sais-tu que lord d'une calcul, le compilateur transtype (cast) les opérandes au plus grand type ?
  1. double deux_tiers = 2/3;
Tu t'attends bien à obtenir un réel, pas un entier. Donc le calcul a bien été celui-là :
  1. float deux_tiers = (float)2/(float)3;
Donc le calcul
signed short int b
++b>0
Devient
signed short int b
++(int)b>(int)0
puisque 0 est un int.

Du code est ajouté lors de la compilation pour que les débordements soient gérés, si on est en mode "optimisations faibles". Ce code est inutile si le programmeur sait exactement ce qu'il fait. On peut alors retirer ces contrôles. Les boucles vides, le code inaccessible (voir code), etc., sont virés.

Attention donc, l'analyse numérique a une vraie valeur ajoutée, et tout s'explique. Ne serait-il pas prétentieux de notre part de juger de la pertinence de ces "optimisations" sans nous remettre un peu en cause ?

  1. { printf("hello");
  2. return;
  3. printf(" world"); }

Et même en croyant maitriser les optimisations, il existe toujours des différences entre plusieurs versions d'un même compilateur. Par exemple, compiler la fameuse racine carrée de karmack avec n'importe quel compilateur et n'importe quel optimisation, le résultat est totalement différent que ce à quoi on est attendu.

Bienvenue dans le monde de la programmation, ou ce qu'on croit maitriser ne représente que 50% de ce qui est fait réellement ^^
Expert Programmation

Oh, la ! DeathAngel. Doucement. Aux effets de bord près, heureusement que le résultat d'un code ne dépend pas du compilateur. Le C est quand même un langage déterministe ! Alors, oui, il y a des effets de bord. D'ailleurs, ce qu'étudiait tantal est justement un effet de bord : le bord des nombres entiers signés courts. ^^

Quant aux codes de J. Carmack, ça marche très bien. Les résultats sont approximatifs au sens mathématique, pas au sens informatique.

Par contre, je te rejoins quand tu dis que les optimisations varient d'un compilo à l'autre. Donc les effets de bord peuvent varier. L'art de bien programmer, c'est donc de toujours savoir se retrouver bien au milieu.

(Oki, Carmack est justement célèbre pour n'y jamais mettre les pieds ;)  )

Je comprend ces histoires de CAST, mais même un int peut déborder et devenir négatif avec une incrémentation. De plus, dire que 0 est vu comme un unsigned int, ne change pas la donne car si on compare à -1, le résultat est le même.

Donc, comme le dit deathangel67300, gcc considère l'incrémentation comme strictement croissante et "oubli" qu'il peut y avoir un débordement possible (bon OK, ce n'est pas une façon de coder :whistle:  ).

Je testerais tout de même avec ICC ce soir, juste pour voir.

zeb a dit :
Les résultats sont approximatifs au sens mathématique, pas au sens informatique.


pour l'avoir testé sur GCC 4.1 et 4.2 l'époque, je peux te dire que c'était loin d'être approximatif au sens mathématique, on était même carrément à des valeurs délirantes.

Après d'autres cas ou tu touchais aux pointeurs devenaient aussi pas mal, par exemple, j'avais ca :

  1. struct BitDart
  2. {
  3. unsigned int position :5;
  4. unsigned int valeur :27;
  5. };
  6.  
  7. int toto=34;
  8. BitDart * bd = &toto;
  9.  
  10. bd->position=1;
  11.  
  12. std::cout << toto << std::endl;


en GCC 4.2 -O0, il m'affichait 33 ce qui est le résultat attendu. -O2 me donnait 34. Le 4.3 34 en permanence, debug ou pas.
Bon d'accord, c'était pas le code super propre je l'avoue ^^ mais c'était le plus simple pour ce que j'avais à faire :p 

J'aimerais bien connaitre les résultats de Tantal_fr avec ICC aussi.
Expert Programmation

tantal_fr a dit :
Je comprend ces histoires de CAST, mais même un int peut déborder et devenir négatif avec une incrémentation.
C ou C++ ne défini pas le comportement à avoir en cas de débordement. Tu ne peux pas affirmer que ton int débordera de telle manière et deviendra négatif, c'est laissé à l'implémentation. Même si 99% du temps ça marche, ça n'est pas dans la norme.
deathangel67300 a dit :
pour l'avoir testé sur GCC 4.1 et 4.2 l'époque, je peux te dire que c'était loin d'être approximatif au sens mathématique, on était même carrément à des valeurs délirantes.

Après d'autres cas ou tu touchais aux pointeurs devenaient aussi pas mal, par exemple, j'avais ca :

  1. struct BitDart
  2. {
  3. unsigned int position :5;
  4. unsigned int valeur :27;
  5. };
  6.  
  7. int toto=34;
  8. BitDart * bd = &toto;
  9.  
  10. bd->position=1;
  11.  
  12. std::cout << toto << std::endl;
C'est exactement pareil. Le cast de toto à BitDart est complêtement indéfini. Sur certaines architectures, en fonction de la taille de tes variables et de l'alignement nécessaire, l'accès à bd pourrai simplement faire planter le programme.
Et de manière générale, tu ne peux rien supposer sur la taille de ta structure. Elle pourrait très bien être sur 64bit, avec seulement une partie de 32bit utilisée. Surtout que les bitfields n'appartiennent à aucune norme C ou C++, donc tu n'a même pas de garantie que position est à coté de valeur.

Maintenant, quand on code un peu mieux (on à toujours des zones d'ombres, mais on dit un peu mieux ce qu'on veut faire) :
  1. union realunion {
  2. struct {
  3. unsigned int position :5;
  4. unsigned int valeur :27;
  5. } dart;
  6. int i;
  7. } a = {.i = 34};
  8. a.dart.position = 1;

et a.i deviens égal à 33, -O2 ou pas. (même si ça reste toujours pas standart)
Expert Programmation

Ah, tiens, ça me rappelle un truc : l'alignement.

Celui-ci peut se faire en 8, 16, 32, 64 bits. On peut le préciser, ou laisser faire les options par défaut. On se retrouve donc avec des struct ou des types standards définissant n octets, avec une fonction size qui te renvoie bien n, mais un emplacement mémoire dans le programme final égal à (n+64-1) modulo 64 dans le cas 64 bits, par exemple.

Oui, l'emplacement d'un char (1 octet), aligné sur un mot (word), sur un proco 64 bits, c'est 8 octets de réservés. Ça peut laisser de la place aux débordements !

batchy a dit :
C ou C++ ne défini pas le comportement à avoir en cas de débordement. Tu ne peux pas affirmer que ton int débordera de telle manière et deviendra négatif, c'est laissé à l'implémentation. Même si 99% du temps ça marche, ça n'est pas dans la norme.


Là ça dépasse clairement mes compétences, enfin pour moi 0111 1111 + 1 = 1000 000. Je pensais que c'étais comme çà partout.

Sinon, je viens d'essayer avec ICC, et là quelques soit les optimisation, on ne tombe jamais en boucle infinie. Voila ce que ça donne en pseudo-asm avec -O2 :

  1. # -- Machine type PW
  2. # mark_description "Intel(R) C++ Compiler Professional for applications running on IA-32, Version 11.0 Build 20081105 %s";
  3. # mark_description "-long_double -O2 -S";
  4. .file "test.c"
  5. .text
  6. ..TXTST0:
  7. # -- Begin main
  8. # mark_begin;
  9. .align 16,0x90
  10. .globl main
  11. main:
  12. ..B1.1: # Preds ..B1.0
  13. pushl %ebp #3.15
  14. movl %esp, %ebp #3.15
  15. andl $-128, %esp #3.15
  16. subl $128, %esp #3.15
  17. pushl $3 #3.15
  18. call __intel_new_proc_init #3.15
  19. # LOE ebx esi edi
  20. ..B1.5: # Preds ..B1.1
  21. stmxcsr 4(%esp) #3.15
  22. orl $32768, 4(%esp) #3.15
  23. ldmxcsr 4(%esp) #3.15
  24. pushl $2 #4.19
  25. pushl $_2__STRING.0.0 #4.19
  26. call printf #6.1
  27. # LOE ebx esi edi
  28. ..B1.2: # Preds ..B1.5
  29. xorl %eax, %eax #9.8
  30. movl %ebp, %esp #9.8
  31. popl %ebp #9.8
  32. ret #9.8
  33. .align 16,0x90
  34. # LOE
  35. # mark_end;
  36. .type main,@function
  37. .size main,.-main
  38. .data
  39. # -- End main
  40. .section .rodata.str1.4, "aMS",@progbits,1
  41. .align 4
  42. .align 4
  43. _2__STRING.0.0:
  44. .byte 37
  45. .byte 100
  46. .byte 10
  47. .byte 0
  48. .type _2__STRING.0.0,@object
  49. .size _2__STRING.0.0,4
  50. .data
  51. .section .note.GNU-stack, ""
  52. # End


Là, la boucle a carrément sauté, ce qui est quand même plus logique, vue qu'au final, cette boucle ne fait rien que boucler sur elle même un certain nombre de fois.
Lassé par la pub ? Créez un compte