Je suis curieux de savoir quelles sont les libertés d’un compilateur lors de l’optimisation. Limitons cette question à GCC et C / C ++ (toute version, toute version de standard):
Est-il possible d’écrire un code qui se comporte différemment selon le niveau d’optimisation avec lequel il a été compilé?
L’exemple que j’ai en tête est d’imprimer différents bits de texte dans différents constructeurs en C ++ et d’obtenir une différence selon que les copies sont éludées (même si je n’ai pas réussi à faire une telle chose).
Le comptage des cycles d’horloge n’est pas autorisé. Si vous avez un exemple pour un compilateur non-GCC, je serais curieux aussi, mais je ne peux pas le vérifier. Points bonus pour un exemple en C. 🙂
Modifier: l’exemple de code doit être conforme à la norme et ne pas contenir de comportement indéfini dès le départ.
Edit 2: Vous avez déjà de bonnes réponses! Permettez-moi de relever un peu les enjeux: le code doit constituer un programme bien formé et être conforme aux normes, et il doit être compilé pour corriger les programmes déterministes à chaque niveau d’optimisation. (Cela exclut des choses comme les conditions de course dans un code multithread mal formé.) J’apprécie également que l’arrondi à virgule flottante puisse être affecté, mais ne tenons pas compte de cela.
Je viens juste d’atteindre 800 points de réputation, alors je pense que je vais me faire une réputation de prime sur le premier exemple complet conforme à (l’esprit) de ces conditions; 25 s’il s’agit d’abuser d’un aliasing ssortingct. (Sous réserve que quelqu’un me montre comment envoyer des primes à quelqu’un d’autre.)
La partie du standard C ++ qui s’applique est le §1.9 “Exécution du programme”. Il lit, en partie:
Les implémentations conformes sont nécessaires pour émuler (uniquement) le comportement observable de la machine abstraite, comme expliqué ci-dessous. …
Une implémentation conforme exécutant un programme bien formé doit produire le même comportement observable que l’une des séquences d’exécution possibles de l’instance correspondante de la machine abstraite avec le même programme et la même entrée. …
Le comportement observable de la machine abstraite est sa séquence de lectures et d’écritures sur les données volatiles et les appels aux fonctions d’E / S de la bibliothèque. …
Donc, oui, le code peut se comporter différemment à différents niveaux d’optimisation, mais (en supposant que tous les niveaux produisent un compilateur conforme), mais ils ne peuvent pas se comporter de manière observable différemment .
EDIT: Permettez-moi de corriger ma conclusion: Oui, le code peut se comporter différemment à différents niveaux d’optimisation tant que chaque comportement est identique à l’un des comportements de la machine abstraite de la norme.
Est-il possible d’écrire un code qui se comporte différemment selon le niveau d’optimisation avec lequel il a été compilé?
Seulement si vous déclenchez un bogue du compilateur.
MODIFIER
Cet exemple se comporte différemment sur gcc 4.5.2:
void foo(int i) { foo(i+1); } main() { foo(0); }
Compilé avec -O0
crée un programme en panne avec une erreur de segmentation.
Compilé avec -O2
crée un programme entrant dans une boucle sans fin.
Les calculs à virgule flottante sont une source mûre de différences. Selon la manière dont les opérations individuelles sont commandées, vous pouvez obtenir plus ou moins d’erreurs d’arrondi.
Un code multithread moins sûr peut avoir des résultats différents selon la façon dont les access à la mémoire sont optimisés, mais c’est essentiellement un bogue dans votre code.
Et comme vous l’avez mentionné, les effets secondaires dans les constructeurs de copies peuvent disparaître lorsque les niveaux d’optimisation changent.
Pour C, presque toutes les opérations sont ssortingctement définies dans la machine abstraite et les optimisations ne sont autorisées que si le résultat observable est exactement celui de cette machine abstraite. Des exceptions à cette règle viennent à l’esprit:
volatile
peuvent ou non être évaluées uniquement pour leurs effets secondaires const
identiques peuvent ou non être repliés en un seul emplacement de mémoire statique OK, mon jeu flagrant pour la prime, en fournissant un exemple concret. Je vais rassembler les éléments des réponses des autres et mes commentaires.
Pour un comportement différent à différents niveaux d’optimisation, “niveau d’optimisation A” doit indiquer gcc -O0
(j’utilise la version 4.3.4, mais peu importe, je pense que toute version même vaguement récente fera la différence Je suis après), et “niveau d’optimisation B” doit désigner gcc -O0 -fno-elide-constructors
.
Le code est simple:
#include struct Foo { ~Foo() { std::cout << "~Foo\n"; } }; int main() { Foo f = Foo(); }
Sortie au niveau d'optimisation A:
~Foo
Sortie au niveau d'optimisation B:
~Foo ~Foo
Le code est totalement légal, mais la sortie dépend de l'implémentation à cause de l'élision du constructeur de la copie, et en particulier, il est sensible à l'indicateur d'optimisation de gcc qui désactive l'élision du scanner de copie.
Notez qu'en règle générale, "optimisation" fait référence aux transformations du compilateur qui peuvent altérer un comportement non défini, non spécifié ou défini par la mise en œuvre, mais pas un comportement défini par la norme. Ainsi, tout exemple qui répond à vos critères est nécessairement un programme dont la sortie est non spécifiée ou définie par l'implémentation. Dans ce cas, il n'est pas précisé par la norme si les développeurs de copie sont éludés, il se trouve que j'ai de la chance que GCC les élimine de manière fiable chaque fois que cela est autorisé, mais a une option pour les désactiver.
Tout comportement indéfini selon la norme peut changer son comportement en fonction du niveau d’optimisation (ou de la phase de lune, d’ailleurs).
L’option -fssortingct-aliasing
peut facilement entraîner des modifications de comportement si vous avez deux pointeurs sur le même bloc de mémoire. Ceci est censé être invalide mais est en fait assez courant.
Étant donné que les appels de constructeur de copies peuvent être optimisés, même s’ils ont des effets secondaires, le fait d’avoir des constructeurs de copie avec des effets secondaires entraînera un comportement différent du code non optimisé et optimisé.
Ce programme C appelle un comportement indéfini, mais affiche des résultats différents dans différents niveaux d’optimisation:
#include /* $ for i in 0 1 2 3 4 do echo -n "$i: " && gcc -O$i xc && ./a.out done 0: 5 1: 5 2: 5 3: -1 4: -1 */ void f(int a) { int b; printf("%d\n", (int)(&a-&b)); } int main() { f(0); return 0; }
J’ai un exemple intéressant dans mon cours de système d’exploitation aujourd’hui. Nous avons analysé des mutex logiciels susceptibles d’être endommagés lors de l’optimisation car le compilateur ne connaît pas l’exécution en parallèle.
Le compilateur peut réorganiser les instructions qui ne fonctionnent pas sur des données dépendantes. Comme j’ai déjà statiné en code parallélisé, cette dépendance est cachée pour le compilateur afin qu’il puisse se casser. L’exemple que j’ai donné pourrait entraîner des difficultés lors du débogage, car la sécurité des threads est rompue et votre code se comporte de manière imprévisible en raison de problèmes de planification du système d’exploitation et d’erreurs d’access simultanées.