Comment savoir si le compilateur a cassé mon code et que faire si c'était le compilateur?

14

De temps en temps, le code C ++ ne fonctionnera pas lorsqu'il est compilé avec un certain niveau d'optimisation. Il peut s'agir d'un compilateur effectuant une optimisation qui rompt le code ou d'un code contenant un comportement non défini qui permet au compilateur de faire ce qu'il ressent.

Supposons que j'ai un morceau de code qui se casse lorsqu'il est compilé avec un niveau d'optimisation supérieur uniquement. Comment savoir si c'est le code ou le compilateur et que dois-je faire si c'est le compilateur?

acéré
la source
43
C'est probablement vous.
littleadv
9
@littleadv, même les versions récentes de gcc et msvc sont pleines de bugs, donc je n'en serais pas si sûr.
SK-logic
3
Vous avez tous les avertissements activés?
@ Thorbjørn Ravn Andersen: Oui, je les ai activés.
sharptooth
3
FWIW: 1) J'essaie de ne rien faire de délicat qui pourrait inciter le compilateur à gâcher, 2) le seul endroit où les indicateurs d'optimisation sont importants (pour la vitesse) est dans le code où le compteur de programme passe une fraction importante de son temps. À moins que vous n'écriviez des boucles de processeur serrées, dans de nombreuses applications, le PC passe essentiellement tout son temps dans les bibliothèques ou les E / S. Dans ce type d'application, les commutateurs / O ne vous aident pas du tout.
Mike Dunlavey

Réponses:

19

Je dirais que c'est une valeur sûre que, dans la grande majorité des cas, c'est votre code, pas le compilateur, qui est cassé. Et même dans le cas extraordinaire où il s'agit du compilateur, vous utilisez probablement une fonctionnalité de langage obscur d'une manière inhabituelle, pour laquelle le compilateur spécifique n'est pas préparé; en d'autres termes, vous pourriez très probablement changer votre code pour être plus idiomatique et éviter le point faible du compilateur.

En tout cas, si vous pouvez prouver que vous avez trouvé un bogue de compilateur (basé sur les spécifications du langage), signalez-le aux développeurs du compilateur, afin qu'ils puissent le corriger un certain temps.

Péter Török
la source
@ SK-logic, assez juste, je n'ai pas de statistiques pour le soutenir. Il est basé sur ma propre expérience, et j'avoue que j'ai rarement repoussé les limites du langage et / ou du compilateur - d'autres peuvent le faire plus souvent.
Péter Török
(1) @ SK-Logic: Je viens de trouver un bogue de compilateur C ++, même code, essayé sur un compilateur et fonctionne, essayé sur un autre, il se casse.
umlcat
8
@umlcat: il s'agit très probablement de votre code en fonction d'un comportement non spécifié; sur un compilateur, il correspond à vos attentes, sur un autre, il ne le fait pas. cela ne signifie pas qu'il est cassé.
Javier
@Ritch Melton, avez-vous déjà utilisé LTO?
SK-logic
1
Je suis d'accord avec Crashworks quand je parle de consoles de jeux. Il n'est pas rare de trouver des bogues de compilateur ésotérique dans cette situation spécifique. Si vous ciblez des PC normaux, cependant, en utilisant un compilateur très utilisé, il est très peu probable que vous tombiez sur un bogue du compilateur que personne n'a vu auparavant.
Trevor Powell du
14

Comme d'habitude, comme pour tout autre bogue: effectuez une expérience contrôlée. Limitez la zone suspecte, désactivez les optimisations pour tout le reste et commencez à varier les optimisations appliquées à ce morceau de code. Une fois que vous obtenez une reproductibilité à 100%, commencez à varier votre code, en introduisant des éléments qui pourraient briser certaines optimisations (par exemple, introduisez un alias de pointeur possible, insérez des appels externes avec des effets secondaires potentiels, etc.). La consultation du code assembleur dans un débogueur peut également être utile.

SK-logic
la source
pourrait aider à quoi? S'il s'agit d'un bogue du compilateur - alors quoi?
littleadv
2
@littleadv, s'il s'agit d'un bogue du compilateur, vous pouvez soit essayer de le corriger (ou simplement le signaler correctement, en détail), ou vous pouvez découvrir comment l'éviter à l'avenir, si vous êtes condamné à continuer à l'utiliser version de votre compilateur pendant un certain temps. Si c'est quelque chose avec votre propre code, l'un des nombreux problèmes limites C ++ - ish, ce type d'examen permet également de corriger un bogue et d'éviter son type à l'avenir.
SK-logic
Donc, comme je l'ai dit dans ma réponse - à part le signalement, il n'y a pas beaucoup de différence de traitement, quelle que soit la faute.
littleadv
3
@littleadv, sans comprendre la nature d'un problème, vous risquez de le rencontrer encore et encore. Et il y a souvent une possibilité de réparer vous-même un compilateur. Et, oui, il n'est pas "improbable" du tout de trouver un bogue dans un compilateur C ++, malheureusement.
SK-logic
10

Examinez le code d'assembly qui en a résulté et voyez s'il fait ce que votre source appelle. N'oubliez pas que les chances sont très élevées que ce soit vraiment votre code en faute d'une manière non évidente.

Loren Pechtel
la source
1
C'est vraiment la seule réponse à cette question. Le travail du compilateur, dans ce cas, est de passer du C ++ au langage assembleur. Vous pensez que c'est le compilateur ... vérifiez que les compilateurs fonctionnent. C'est aussi simple que cela.
old_timer
7

En plus de 30 ans de programmation, le nombre de bogues de compilation (génération de code) authentiques que j'ai trouvés n'est toujours que de 10. Le nombre de mes bogues (et ceux d'autres personnes) que j'ai trouvés et corrigés au cours de la même période est probablement > 10 000. Ma "règle générale" est alors que la probabilité qu'un bogue donné soit dû au compilateur est <0,001.

Paul R
la source
1
Tu es chanceux. Ma moyenne est d'environ 1 très mauvais bug par mois, et les problèmes mineurs de limite sont beaucoup plus fréquents. Et plus le niveau d'optimisation que vous utilisez est élevé, plus les chances d'erreur du compilateur sont élevées. Si vous essayez d'utiliser -O3 et LTO, vous seriez très chanceux de ne pas en trouver deux en un rien de temps. Et je ne compte ici que les bogues des versions - en tant que développeur de compilateurs, je fais face à beaucoup plus de problèmes de ce genre dans mon travail, mais cela ne compte pas. Je sais juste combien il est facile de bousiller un compilateur.
SK-logic
2
25 ans et j'en ai aussi vu beaucoup. Les compilateurs empirent chaque année.
old_timer
5

J'ai commencé à écrire un commentaire, puis j'ai décidé que c'était trop long et trop précis.

Je dirais que c'est votre code qui est cassé. Dans le cas peu probable où vous auriez découvert un bogue dans le compilateur - vous devriez le signaler aux développeurs du compilateur, mais c'est là que la différence s'arrête.

La solution consiste à identifier le construit incriminé et à le refactoriser pour qu'il fasse différemment la même logique. Cela résoudrait très probablement le problème, que le bogue soit de votre côté ou dans le compilateur.

littleadv
la source
5
  1. Relisez attentivement votre code. Assurez-vous que vous ne faites rien avec des effets secondaires dans ASSERTs ou dans d'autres instructions spécifiques au débogage (ou plus généralement à la configuration). Souvenez-vous également que dans une version de débogage, la mémoire est initialisée différemment - des valeurs de pointeur révélatrices que vous pouvez vérifier ici: Débogage - Représentations d'allocation de mémoire . Lorsque vous exécutez à partir de Visual Studio, vous utilisez presque toujours le débogage (même en mode de publication), sauf si vous spécifiez explicitement avec une variable d'environnement que ce n'est pas ce que vous voulez.
  2. Vérifiez votre build. Il est courant d'avoir des problèmes avec des builds complexes ailleurs que dans le compilateur réel - les dépendances étant souvent le coupable. Je sais que "avez-vous essayé de reconstruire complètement" est presque une réponse aussi exaspérante que "avez-vous essayé de réinstaller Windows", mais cela aide souvent. Essayez: a) Redémarrage. b) Supprimer MANUELLEMENT tous vos fichiers intermédiaires et de sortie et les reconstruire.
  3. Parcourez votre code pour rechercher les emplacements potentiels où vous pourriez invoquer un comportement non défini. Si vous travaillez en C ++ depuis un certain temps, vous saurez qu'il y a des endroits où vous pensez "Je ne suis pas ENTIÈREMENT sûr de pouvoir supposer que ..." - google ou demandez ici à ce sujet type de code pour voir s'il s'agit d'un comportement indéfini ou non.
  4. Si cela ne semble toujours pas être le cas, générez une sortie prétraitée pour le fichier à l'origine des problèmes. Une expansion de macro inattendue peut provoquer toutes sortes de plaisir (je me souviens du moment où un collègue a décidé qu'une macro du nom de H serait une bonne idée ...). Examinez la sortie prétraitée pour les changements inattendus entre les configurations de votre projet.
  5. Dernier recours - maintenant vous êtes vraiment dans le pays des bogues du compilateur - regardez la sortie de l'assembly. Cela peut prendre un peu de fouille et de combat juste pour avoir une idée de ce que fait réellement l'assemblage, mais c'est en fait assez informatif. Vous pouvez également utiliser les compétences que vous avez acquises ici pour évaluer les micro-optimisations, afin que tout ne soit pas perdu.
Joris Timmermans
la source
+1 pour "comportement indéfini". J'ai été mordu par celui-là. A écrit du code qui dépendait int + intdu débordement exactement comme s'il était compilé en une instruction ADD matérielle. Cela fonctionnait très bien lors de la compilation avec une ancienne version de GCC, mais pas lors de la compilation avec le nouveau compilateur. Apparemment, les gens sympathiques de GCC ont décidé que, comme le résultat d'un débordement d'entier n'est pas défini, leur optimiseur pourrait fonctionner en supposant que cela ne se produise jamais. Il a optimisé une branche importante dès la sortie du code.
Solomon Slow
2

Si vous voulez savoir s'il s'agit de votre code ou du compilateur, vous devez parfaitement connaître la spécification de C ++.

Si le doute persiste, vous devez parfaitement connaître l'assemblage x86.

Si vous n'êtes pas d'humeur à apprendre les deux à la perfection, c'est certainement un comportement indéfini que votre compilateur résout différemment selon le niveau d'optimisation.

mouviciel
la source
(+1) @mouviciel: Cela dépend aussi si la fonctionnalité est supportée par le compilateur, même si c'est dans la spécification. J'ai un bug bizarre avec gcc. Je déclare une "structure c simple" avec un "pointeur de fonction", son autorisé dans la spécification, mais fonctionne dans certaines situations, et ne fonctionne pas dans un autre.
umlcat
1

Obtenir une erreur de compilation sur du code standard ou une erreur de compilation interne est plus probable que les optimiseurs se trompent. Mais j'ai entendu parler de compilateurs optimisant les boucles en oubliant de manière incorrecte certains effets secondaires dus à une méthode.

Je n'ai aucune suggestion sur la façon de savoir si c'est vous ou le compilateur. Vous pouvez essayer un autre compilateur.

Un jour, je me demandais si c'était mon code ou non et quelqu'un m'a suggéré valgrind. J'ai passé les 5 ou 10 minutes pour exécuter mon programme avec lui (je pense que je l' valgrind --leak-check=yes myprog arg1 arg2ai fait mais j'ai joué avec d'autres options) et il m'a immédiatement montré UNE ligne qui est exécutée sous un cas spécifique, ce qui était le problème. Ensuite, mon application s'est bien déroulée depuis, sans accidents, erreurs ou comportements étranges. valgrind ou un autre outil comme celui-ci est un bon moyen de savoir si c'est votre code.

Note latérale: je me demandais une fois pourquoi les performances de mon application étaient nulles. Il s'est avéré que tous mes problèmes de performances étaient également sur une seule ligne. J'ai écrit for(int i=0; i<strlen(sz); ++i) {. Le sz était de quelques mb. Pour une raison quelconque, le compilateur a exécuté strlen à chaque fois, même après l'optimisation. Une ligne peut être un gros problème. Des performances aux plantages


la source
1

Une situation de plus en plus courante est que les compilateurs cassent le code écrit pour les dialectes de C qui prennent en charge les comportements non prescrits par la norme et permettent au code ciblant ces dialectes d'être plus efficace que le code strictement conforme. Dans un tel cas, il serait injuste de décrire comme un code "cassé" qui serait fiable à 100% sur les compilateurs qui ont implémenté le dialecte cible, ou de décrire comme "cassé" le compilateur qui traite un dialecte qui ne prend pas en charge la sémantique requise . Au lieu de cela, les problèmes proviennent simplement du fait que le langage traité par les compilateurs modernes avec les optimisations activées diffère des dialectes qui étaient autrefois populaires (et sont toujours traités par de nombreux compilateurs avec les optimisations désactivées, ou par certains même avec les optimisations activées).

Par exemple, beaucoup de code est écrit pour les dialectes qui reconnaissent comme légitimes un certain nombre de modèles d'alias de pointeur non requis par l'interprétation de la norme par gcc, et utilise ces modèles pour permettre une traduction simple du code pour être plus lisible et efficace que ce qui serait possible selon l'interprétation de la norme C par la gcc. Un tel code peut ne pas être compatible avec gcc, mais cela ne signifie pas qu'il est cassé. Il s'appuie simplement sur des extensions que gcc ne prend en charge qu'avec les optimisations désactivées.

supercat
la source
Eh bien, bien sûr, il n'y a rien de mal à coder les extensions X + C et Y standard C, tant que cela vous offre des avantages significatifs, vous savez que vous l'avez fait et vous l'avez documenté en détail. Malheureusement, les trois conditions ne sont normalement pas remplies , et il est donc juste de dire que le code est cassé.
Déduplicateur
@Deduplicator: les compilateurs C89 ont été promus comme étant à compatibilité ascendante avec leurs prédécesseurs, et de même pour C99, etc. Bien que C89 n'impose aucune exigence sur les comportements qui avaient été précédemment définis sur certaines plates-formes mais pas sur d'autres, la compatibilité ascendante suggérerait que les compilateurs C89 pour les plates-formes qui avaient considéré les comportements comme définis devraient continuer de le faire; la justification de la promotion des types courts non signés en signes suggérerait que les auteurs de la norme s'attendaient à ce que les compilateurs se comportent de cette manière, que la norme l'ait ou non imposé. Plus loin ...
supercat
... une interprétation stricte des règles de crénelage jetterait la compatibilité ascendante par la fenêtre et rendrait de nombreux types de code inapplicables, mais quelques légers ajustements (par exemple, en identifiant certains modèles où un crénelage de type croisé devrait être attendu et donc autorisé) résoudraient les deux problèmes . Le but déclaré de la règle était d'éviter d'exiger des compilateurs qu'ils fassent des hypothèses d'aliasing "pessimistes", mais étant donné "float x", devrait-on supposer que "foo ((int *) & x)" pourrait modifier x même si "foo" ne le fait pas 'n'écris sur aucun pointeur de type' float * "ou" char * "être considéré comme" pessimiste "ou" évident "?
supercat
0

Isolez l'endroit problématique et comparez le comportement observé à ce qui devrait se produire selon les spécifications de la langue. Certainement pas facile, mais c'est ce que vous devez faire pour savoir (et pas seulement assumer ).

Je ne serais probablement pas si méticuleux. Je demanderais plutôt au forum de support du compilateur / à la liste de diffusion. Si c'est vraiment un bogue dans le compilateur, ils pourraient le corriger. Ce serait probablement mon code de toute façon. Par exemple, les spécifications de langue concernant la visibilité de la mémoire dans le filetage peuvent être assez contre-intuitives, et elles ne peuvent devenir apparentes que lors de l'utilisation de drapeaux d'optimisation spécifiques, sur un matériel spécifique (!). Certains comportements pourraient être indéfinis par la spécification, donc cela pourrait fonctionner avec certains compilateurs / certains indicateurs, et ne pas fonctionner avec d'autres, etc.

Joonas Pulakka
la source
0

Votre code a très probablement un comportement non défini (comme d'autres l'ont expliqué, vous êtes beaucoup plus susceptible d'avoir des bogues dans votre code que dans le compilateur, même si les compilateurs C ++ sont si complexes qu'ils ont des bogues; même la spécification C ++ a des bogues de conception) . Et UB peut être là même si l'exécutable compilé se trouve fonctionner (par malchance).

Vous devriez donc lire le blog de Lattner Ce que chaque programmeur C devrait savoir sur le comportement non défini (la plupart s'applique également à C ++ 11).

L' outil valgrind et les -fsanitize= options d'instrumentation récentes de GCC (ou Clang / LLVM ) devraient également être utiles. Et bien sûr, activez tous les avertissements:g++ -Wall -Wextra

Basile Starynkevitch
la source