Aujourd’hui, j’ai eu une discussion avec un de mes amis et nous avons débattu pendant quelques heures de «l’optimisation du compilateur».
Je défendais le point que parfois, une optimisation du compilateur pouvait introduire des bugs ou au moins un comportement indésirable.
Mon ami était totalement en désaccord, affirmant que "les compilateurs sont construits par des gens intelligents et font des choses intelligentes" et qu’ils ne peuvent donc jamais se tromper.
Il ne m'a pas convaincu du tout, mais je dois admettre que je manque d'exemples concrets pour renforcer mon argument.
Qui est juste ici? Si je le suis, avez-vous un exemple concret dans lequel une optimisation du compilateur a généré un bogue dans le logiciel résultant? Si je me trompe, dois-je arrêter la programmation et apprendre la pêche?
Les optimisations du compilateur peuvent introduire des bugs ou des comportements indésirables. C'est pourquoi vous pouvez les désactiver.
Un exemple: un compilateur peut optimiser l'accès en lecture/écriture à un emplacement de mémoire, en éliminant par exemple les lectures et les écritures en double ou en réorganisant certaines opérations. Si l'emplacement mémoire en question n'est utilisé que par un seul thread et est en fait de la mémoire, cela peut être correct. Toutefois, si l'emplacement de la mémoire est un registre de périphérique matériel IO, il peut être totalement erroné de réordonner ou d'éliminer les écritures. Dans cette situation, vous devez normalement écrire du code en sachant que le compilateur peut "l'optimiser", et donc en sachant que l'approche naïve ne fonctionne pas.
Mise à jour: Comme Adam Robinson l'a souligné dans un commentaire, le scénario que je décris ci-dessus est davantage une erreur de programmation qu'une erreur d'optimisation. Mais ce que j’essayais d’illustrer, c’est que certains programmes, qui sont par ailleurs corrects, associés à certaines optimisations, qui fonctionnent sinon correctement, peuvent introduire des erreurs dans le programme lorsqu’elles sont combinées. Dans certains cas, la spécification du langage indique "Vous devez procéder de cette façon, car ce type d'optimisation peut survenir et votre programme va échouer", auquel cas il s'agit d'un bogue dans le code. Mais parfois, un compilateur possède une fonctionnalité d'optimisation (généralement facultative) pouvant générer un code incorrect, car le compilateur tente trop d'optimiser le code ou ne peut pas détecter que l'optimisation est inappropriée. Dans ce cas, le programmeur doit savoir quand il est sûr d'activer l'optimisation en question.
Autre exemple: Le noyau linux avait un bogue où un pointeur potentiellement NULL était déréférencé avant qu'un test pour ce pointeur ne soit null. Cependant, dans certains cas, il était possible de mapper la mémoire sur l'adresse zéro, permettant ainsi le déréférencement. Le compilateur, après avoir constaté que le pointeur était déréférencé, a supposé qu'il ne pouvait pas être NULL, puis a supprimé le test NULL plus tard et tout le code de cette branche. Ceci introduisit une vulnérabilité de sécurité dans le code, car la fonction continuerait à utiliser un pointeur non valide contenant des données fournies par l'attaquant. Dans les cas où le pointeur était légitimement nul et que la mémoire n'était pas mappée sur l'adresse zéro, le noyau restait toujours comme auparavant. Donc avant l'optimisation, le code contenait un bogue; après, il en contenait deux et l’un d’eux autorisait un exploit racine local.
CERT a une présentation intitulée "Optimisations dangereuses et la perte de causalité" de Robert C. Seacord qui répertorie de nombreuses optimisations qui introduisent (ou exposent) des bogues dans les programmes. Il aborde les différents types d'optimisation possibles, de "faire ce que le matériel fait" à "intercepter tous les comportements possibles non définis" à "faire tout ce qui n'est pas interdit".
Quelques exemples de code parfaitement corrects jusqu'à ce qu'un compilateur à l'optimisation agressive mette la main dessus:
Vérification du débordement
// fails because the overflow test gets removed
if (ptr + len < ptr || ptr + len > max) return EINVAL;
Utilisation de l'artithmétique de débordement:
// The compiler optimizes this to an infinite loop
for (i = 1; i > 0; i += i) ++j;
Effacement de la mémoire des informations sensibles:
// the compiler can remove these "useless writes"
memset(password_buffer, 0, sizeof(password_buffer));
Le problème, c’est que depuis des décennies, les compilateurs ont été moins agressifs en matière d’optimisation, ce qui a permis à des générations de programmeurs C d’apprendre et de comprendre des choses comme l’addition du complément à taille fixe et son débordement. Ensuite, la norme de langage C est modifiée par les développeurs du compilateur et les règles subtiles changent, même si le matériel ne change pas. La spécification en langage C est un contrat entre les développeurs et les compilateurs, mais les termes de l'accord sont susceptibles de changer avec le temps et tout le monde ne comprend pas tous les détails, ni ne reconnaît que ces détails sont même raisonnables.
C'est pourquoi la plupart des compilateurs proposent des indicateurs pour désactiver (ou activer) les optimisations. Votre programme est-il écrit avec la compréhension que des entiers pourraient déborder? Ensuite, vous devez désactiver les optimisations de débordement, car elles peuvent introduire des bogues. Votre programme évite-t-il strictement les pointeurs de crénelage? Ensuite, vous pouvez activer les optimisations selon lesquelles les pointeurs ne sont jamais aliasés. Votre programme essaie-t-il d'effacer la mémoire pour éviter la fuite d'informations? Oh, dans ce cas, vous n'avez pas de chance: vous devez soit désactiver le retrait de code mort, soit vous devez savoir, à l'avance, que votre compilateur va éliminer votre code "mort" et utiliser du travail. -around pour cela.
Lorsqu'un bug disparaît en désactivant les optimisations, la plupart du temps, c'est toujours votre faute
Je suis responsable d'une application commerciale, écrite principalement en C++ - démarrée avec VC5, transférée au début sur VC6, et maintenant transférée avec succès sur VC2008. Il a dépassé le million de lignes au cours des 10 dernières années.
Pendant ce temps, je pouvais confirmer un seul bogue de génération de code qui se produisait lorsque des optimisations agressives étaient activées.
Alors pourquoi je me plains? Parce que dans le même temps, il y avait des dizaines de bugs qui m'ont fait douter du compilateur - mais cela s'est avéré être une compréhension insuffisante de la norme C++. La norme laisse de la place aux optimisations que le compilateur peut utiliser ou non.
Au fil des ans, sur différents forums, j'ai vu de nombreux articles blâmer le compilateur, se révélant finalement être des bogues dans le code d'origine. Nul doute que beaucoup d’entre eux cachent des bogues qui nécessitent une compréhension détaillée des concepts utilisés dans la norme, mais des bogues de code source néanmoins.
Pourquoi je réponds si tard: arrêtez de blâmer le compilateur avant d'avoir confirmé que c'est bien le compilateur.
L'optimisation du compilateur (et de l'exécution) peut certainement introduire comportement non désiré mais - au moins devrait ne se produire que si vous vous appuyez sur un comportement non spécifié (ou si vous faites des suppositions incorrectes à propos d'un comportement bien spécifié).
Maintenant au-delà de cela, bien sûr, les compilateurs peuvent avoir des bogues. Certaines d'entre elles peuvent être liées à des optimisations, et les implications pourraient être très subtiles - en fait, elles sont susceptibles d'être, car des bogues évidents ont plus de chances d'être corrigés.
En supposant que vous incluiez des JIT en tant que compilateurs, j'ai vu des bogues dans les versions publiées du JET .NET et de la JVM de Hotspot (je n'ai pas les détails pour le moment, malheureusement) qui étaient reproductibles dans des situations particulièrement étranges. Si elles étaient dues à des optimisations particulières ou non, je ne sais pas.
Pour combiner les autres posts:
Les compilateurs ont parfois des bogues dans leur code, comme la plupart des logiciels. L'argument des "personnes intelligentes" est totalement dénué de pertinence, car les satellites de la NASA et d'autres applications conçues par des personnes intelligentes ont aussi des bogues. Le codage utilisé pour l'optimisation est différent de celui qui ne l'est pas. Par conséquent, si le bogue se trouve dans l'optimiseur, votre code optimisé peut contenir des erreurs alors que votre code non optimisé ne le fera pas.
Comme M. Shiny et New l'ont souligné, il est possible qu'un code naïf en ce qui concerne les problèmes de simultanéité et/ou de synchronisation s'exécute de manière satisfaisante sans optimisation, mais échoue avec l'optimisation, ce qui peut modifier le calendrier d'exécution. Vous pouvez attribuer un tel problème au code source, mais si celui-ci ne se manifeste que lorsqu'il est optimisé, certaines personnes pourraient en vouloir à l'optimisation.
Un exemple: il y a quelques jours, quelqu'un découvert que gcc 4.5 avec l'option -foptimize-sibling-calls
(impliquée par -O2
) produisait un exécutable Emacs qui divisait les valeurs par défaut au démarrage.
Ceci a apparemment corrigé depuis.
Je n'ai jamais entendu parler ou utilisé un compilateur dont les directives ne pouvaient pas modifier le comportement d'un programme. En règle générale, il s'agit d'un bonne chose, mais vous devez lire le manuel.
ET j'ai eu récemment une situation où une directive du compilateur a "supprimé" un bogue. Bien sûr, le bogue existe toujours, mais j’ai une solution de contournement temporaire jusqu’à ce que je corrige le programme correctement.
Oui. Un bon exemple est le schéma de verrouillage à double vérification. En C++, il est impossible d'implémenter en toute sécurité un verrouillage à double vérification, car le compilateur peut réorganiser les instructions de manière judicieuse dans un système mono-thread mais pas dans un système multi-thread. Une discussion complète peut être trouvée sur http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
Est-ce probable? Pas dans un produit majeur, mais c'est certainement possible. Les optimisations du compilateur sont du code généré. peu importe d'où vient le code (vous l'écrivez ou quelque chose le génère), il peut contenir des erreurs.
Je l'ai rencontré à quelques reprises avec un compilateur récent construisant l'ancien code. L'ancien code fonctionnait, mais reposait parfois sur un comportement indéfini, tel qu'une surcharge ou une surcharge d'opérateur mal défini. Cela fonctionnerait dans la version de débogage VS2003 ou VS2005, mais dans la version en cours, il planterait.
En ouvrant l’Assemblée générée, il était clair que le compilateur venait de supprimer 80% des fonctionnalités de la fonction en question. Réécrire le code pour ne pas utiliser un comportement indéfini l'a effacé.
Exemple plus évident: VS2008 vs GCC
Déclaré:
Function foo( const type & tp );
Appelé:
foo( foo2() );
où foo2()
renvoie un objet de la classe type
;
A tendance à planter dans GCC car l'objet n'est pas alloué dans la pile dans ce cas, mais VS optimise son utilisation pour résoudre ce problème et cela fonctionnera probablement.
La création d'alias peut entraîner des problèmes avec certaines optimisations. C'est pourquoi les compilateurs ont la possibilité de désactiver ces optimisations. De Wikipedia :
Pour permettre ces optimisations de manière prévisible, la norme ISO pour le langage de programmation C (y compris sa plus récente édition C99) spécifie qu'il est interdit (à quelques exceptions près) que des pointeurs de types différents fassent référence au même emplacement mémoire. Cette règle, connue sous le nom de "aliasing strict", permet une augmentation impressionnante des performances [citation requise], mais est connue pour casser du code par ailleurs valide. Plusieurs projets de logiciels violent intentionnellement cette partie de la norme C99. Par exemple, Python 2.x l'a fait pour implémenter le comptage de références, [1] et a nécessité des modifications des structures d'objet de base dans Python 3 pour permettre cette optimisation. C'est ce que fait le noyau Linux, car l'aliasing strict pose des problèmes d'optimisation du code en ligne. [2] Dans de tels cas, lorsqu'elle est compilée avec gcc, l'option -fno-strict-aliasing est invoquée pour empêcher les optimisations non désirées ou non valides pouvant générer un code incorrect.
Oui, les optimisations du compilateur peuvent être dangereuses. Les projets logiciels en temps réel habituellement difficiles interdisent les optimisations pour cette raison même. Quoi qu'il en soit, connaissez-vous un logiciel sans bugs?
Des optimisations agressives peuvent mettre en cache ou même faire des hypothèses étranges avec vos variables. Le problème n'est pas seulement avec la stabilité de votre code, mais ils peuvent aussi tromper votre débogueur. J'ai vu plusieurs fois un débogueur ne parvenant pas à représenter le contenu de la mémoire car certaines optimisations conservaient une valeur variable dans les registres du micro.
La même chose peut arriver à votre code. L'optimisation place une variable dans un registre et n'écrit pas dans la variable tant qu'elle n'est pas terminée. Maintenant, imaginez à quel point les choses peuvent être différentes si votre code contient des pointeurs vers des variables dans votre pile et s'il comporte plusieurs threads.
C'est théoriquement possible, bien sûr. Mais si vous ne faites pas confiance aux outils pour faire ce qu'ils sont supposés faire, pourquoi les utiliser? Mais tout de suite, quiconque se disputer de la position de
"Les compilateurs sont construits par des personnes intelligentes et font des choses intelligentes" et peuvent donc ne jamais se tromper.
fait un argument stupide.
Donc, jusqu'à ce que vous ayez des raisons de croire qu'un compilateur le fait, pourquoi vous en tenir à cela?
Cela peut arriver. Cela a même affecté Linux .
Je conviens certainement que c’est idiot de dire que les compilateurs sont écrits par des "personnes intelligentes" et qu’ils sont donc infaillibles. Des gens intelligents ont également conçu les ponts Hindenberg et Tacoma Narrows. Même s'il est vrai que les auteurs de compilations comptent parmi les programmeurs les plus intelligents, il est également vrai que les compilateurs comptent parmi les programmes les plus complexes. Bien sûr, ils ont des insectes.
D'autre part, l'expérience nous dit que la fiabilité des compilateurs commerciaux est très élevée. De nombreuses fois, on m'a dit que la raison pour laquelle le programme ne fonctionnait pas DOIT ÊTRE à cause d'un bogue dans le compilateur, car il l'a vérifié très attentivement et il est sûr que c'est 100% correct et ensuite nous constatons qu'en fait le programme a une erreur et non le compilateur. J'essaie de penser à des moments où j'ai personnellement rencontré quelque chose qui, j'en étais vraiment sûr, était une erreur du compilateur, et je ne me souviens que d'un exemple.
Donc en général: faites confiance à votre compilateur. Mais ont-ils déjà tort? Sûr.
Si je me souviens bien, au début de Delphi 1, il y avait un bogue qui inversait les résultats de Min et Max. Il y avait aussi un bogue obscur avec certaines valeurs en virgule flottante uniquement lorsque la valeur en virgule flottante était utilisée dans une dll. Certes, cela fait plus d’une décennie, ma mémoire est peut-être un peu floue.
L'optimisation du compilateur peut révéler (ou activer) des bogues en sommeil (ou cachés) dans votre code. Votre code C++ contient peut-être un bogue que vous ne connaissez pas, mais que vous ne le voyez pas. Dans ce cas, il s'agit d'un bogue caché ou en veille, car cette branche du code n'est pas exécutée [nombre de fois suffisant].
La probabilité d'un bogue dans votre code est beaucoup plus grande (des milliers de fois plus) qu'un bogue dans le code du compilateur: Parce que les compilateurs sont testés de manière approfondie. Par TDD et pratiquement par toutes les personnes qui les utilisent depuis leur sortie!). Il est donc pratiquement improbable qu'un bogue soit découvert par vous et non pas littéralement des centaines de milliers de fois qu'il soit utilisé par d'autres personnes.
Un bug dormant ou un bug caché est juste un bug qui n'est pas encore révélé au programmeur. Les personnes pouvant prétendre que leur code C++ ne contient pas de bogues (cachés) sont très rares. Cela nécessite des connaissances en C++ (très peu de gens peuvent prétendre à cela) et des tests approfondis du code. Il ne s'agit pas seulement du programmeur, mais du code lui-même (le style de développement). Être sujet à des bogues est dans le caractère du code (sa rigueur dans le test) et/ou du programmeur (sa discipline est testée et sa connaissance du C++ et de la programmation).
Security + Bugs de concurrence: Cela est encore pire si nous incluons la concurrence et la sécurité en tant que bogues. Mais après tout, ce sont des bugs. Écrire un code qui en premier lieu est exempt de bogues en termes de concurrence et de sécurité est presque impossible. C'est pourquoi il y a toujours un bogue dans le code, qui peut être révélé (ou oublié) dans l'optimisation du compilateur.
J'ai rencontré un problème dans .NET 3.5 si vous construisez avec l'optimisation, ajoutez une autre variable à une méthode portant le même nom qu'une variable existante du même type dans la même portée, puis l'une des deux (nouvelle ou ancienne variable) ne le sera pas. être valide au moment de l'exécution et toutes les références à la variable non valide sont remplacées par des références à l'autre.
Ainsi, par exemple, si j'ai abcd de type MyCustomClass et si abdc de type MyCustomClass et que je règle abcd.a = 5 et abdc.a = 7, les deux variables auront la propriété a = 7. Pour résoudre le problème, les deux variables doivent être supprimées, le programme compilé (heureusement sans erreur), puis elles doivent être rajoutées.
Je pense avoir rencontré ce problème plusieurs fois avec .NET 4.0 et C # lors de l'exécution d'applications Silverlight également. Lors de mon dernier travail, nous avons rencontré le problème assez souvent en C++. C’est peut-être parce que les compilations ont pris 15 minutes; nous ne construisions donc que les bibliothèques dont nous avions besoin, mais le code optimisé était parfois exactement identique à la version précédente, même si un nouveau code avait été ajouté et aucune erreur de construction n’avait été signalée.
Oui, les optimiseurs de code sont conçus par des personnes intelligentes. Ils sont également très compliqués, il est donc courant d’avoir des bogues. Je suggère de tester pleinement toute version optimisée d'un grand produit. En règle générale, les produits à usage limité ne valent pas une version complète, mais ils doivent tout de même être testés pour s'assurer qu'ils s'acquittent correctement de leurs tâches courantes.
Tout ce que vous pouvez imaginer faire avec ou à un programme introduira des bugs.
Je travaille sur une grande application d'ingénierie et, de temps à autre, nous ne voyons que des blocages et d'autres problèmes signalés par les clients. Notre code contient 37 fichiers (sur environ 6000) où nous avons ceci en haut du fichier, pour désactiver l'optimisation afin de réparer de tels plantages:
#pragma optimize( "", off)
(Nous utilisons Microsoft Visual C++ en version native, 2015, mais c'est le cas pour presque tous les compilateurs, à l'exception peut-être de la mise à jour 2 d'Intel Fortran 2016 pour laquelle nous n'avons pas encore optimisé.)
Si vous effectuez une recherche sur le site de commentaires Microsoft Visual Studio, vous pouvez également y trouver quelques bogues d'optimisation. Nous enregistrons parfois certaines des nôtres (si vous pouvez le reproduire assez facilement avec une petite section de code et que vous êtes prêt à prendre le temps) et elles sont réparées, mais malheureusement, d'autres sont réintroduites. sourires
Les compilateurs sont des programmes écrits par des gens, et tout gros programme a des bugs, croyez-moi là-dessus. Les options d'optimisation du compilateur comportent certainement des bogues et l'activation de l'optimisation peut certainement introduire des bogues dans votre programme.
Des optimisations plus nombreuses et plus agressives pourraient être activées si le programme que vous compilez dispose d'une bonne suite de tests. Ensuite, il est possible d’exécuter cette suite et d’être un peu plus sûr que le programme fonctionne correctement. En outre, vous pouvez préparer vos propres tests qui correspondent étroitement à ce que vous envisagez de faire en production.
Il est également vrai que tout programme important peut avoir (et probablement même en réalité) des bogues indépendamment des commutateurs que vous utilisez pour le compiler.