Les ‘énigmes’ sont de petits problèmes amusants qui permettent de découvrir les limites d’un langage de programmation ou bien des fonctionnalités mal connues.
Notre première énigme concerne le langage C++ et surtout l’utilisation des opérateurs ‘<<
‘. Voici les sources du programme que nous allons regarder:
#include <iostream> using namespace std; int main () { int i = 0; cout << "i++: " << i++ << i++ << i++ << i++ << endl; i = 0; cout << "++i: " << ++i << ++i << ++i << ++i << endl; cout << "L'adresse de i est: " << &i << endl; return 0; }
Losqu’on compile le programme en faisant ‘g++ -o main main.c
‘, l’exécution donne:
i++: 3210 ++i: 4321 L'adresse de i est: 0xbfb1c44c
Pourtant, lorsqu’on rajoute l’optimisation en faisant ‘g++ -O2 -o main main.c
‘, l’exécution donne:
i++: 3210 ++i: 4444 L'adresse de i est: 0xbf9af0cc
Question: Comment expliquez-vous cette différence à l’exécution ?
Le comportement de ce code n’est pas défini. En effet, en activant les warnings:
firas@aoba ~ % g++ -Wall -o test test.cpp
test.cpp: In function ‘int main()’:
test.cpp:6: warning: operation on ‘i’ may be undefined
test.cpp:8: warning: operation on ‘i’ may be undefined
C’est dû au fait qu’il n’y a pas de point de séquence après chaque >> ou ++. Le compilateur est donc libre de les appliquer dans l’ordre qu’il veut (soit un à la fois dans l’ordre où ils apparaissent, soit tous d’un coup à la fin de l’instruction, par exemple).
En règle générale, si un programme se comporte différemment à différents niveaux d’optimisation, c’est soit un bug du compilateur (peu probable), soit un bug du code. Les standards sont justement là pour que le comportement d’un programme soit bien défini. En les suivant, on n’a pas ce problème.
Effectivement, c’est une partie de la réponse.
La norme de C++ précise que l’ordre d’évaluation des opérateurs ‘
<<
‘ n’est pas fixé et peut dépendre du compilateur utilisé. Ce qui veut dire qu’il ne faut pas utiliser d’effets de bords dans ce genre d’expression (or c’est ce qui est fait ici).Quoiqu’il en soit, cela montre qu’activer les warnings est toujours un bon réflexe car ce genre de comportements hasardeux est presque toujours détecté par le compilateur.
Cependant, il y a une deuxième couche à cette énigme car si on retire la ligne qui affiche l’adresse de ‘
i
‘, le comportement non-optimisé et optimisé sont identiques.Sauriez-vous l’expliquer ?
Ne connaissant que très peu le code de GCC, je ne suis pas capable de cibler la partie de code incriminée, mais la présence d’un LEA sur ‘i’ semble faire GCC se comporter comme si la variable était volatile, créant un jonglage entre mémoire et registres à chaque incrémentation (et gardant les résultats intermédiaires dans %edx, %edi, %esi et %ebx chez moi). Ce sont les valeurs de ces registres qui sont passés en paramètres lors des l’output. Sans LEA, ‘i’ est directement 4 fois incrémentée (à noter qu’il y a bien incrémentation 4 fois d’affilé, il n’y a vraiment aucune optimisations…) et la valeur passée lors de l’output.
Évidement, en O2, j’imagine que leur SCCP n’a aucun problème avec ce code (qui comporte bel et bien les constantes inline).
Fleury: sais-tu quelle partie du code provoque ce comportement (avec/sans affichage de l’adresse) ? J’y jetterais bien un coup d’œil.
Oui, c’est exactement ça.
Pour une raison ou une autre, GCC taggue la variable ‘
i
‘ comme ‘volatile
‘ et provoque le comportement que l’on observe lorsqu’il n’y a pas d’optimisation. Par contre, lorsqu’on active l’optimisation, le compilateur s’aperçoit que c’était un faux-positif et considère ‘i
‘ comme une variable normale.On peut d’ailleurs forcer l’un ou l’autre des comportements en retirant la ligne qui fait appel à ‘
&i
‘ et en tagguant ‘i
‘ comme ‘volatile
‘ ou ‘register
‘ (comportement par défaut) lors de sa déclaration.Sinon, je n’ai pas cherché à savoir pourquoi GCC déduit que ‘
i
‘ est volatile dans le premier cas. Mais je suppose que c’est au niveau des passes GIMPLE que cela se passe et non au niveau des passes RTL. Donc, regarder l’assembleur final n’aidera probablement pas. Il faudrait jouer avec les options ‘-fdump-tree-xxx
‘ et ‘-fdump-rtl-xxx
‘ pour voir à quelle passe commence à apparaître la différence.