j'ai un code qui fonctione en debug mais pas en optimisé et uniquement avec clang pas avec gcc. je voulais savoir votre avis si c'est un bug ou si c'est normal ou je sais pas. j'ai fait un code minimal pour vous faire la demo.
#include <stdio.h>
int test()
{
while(1)
{
for(int i=10; i<100; i++)
if ( i*i > i*i*i )
return 1;
}
return 0;
}
int main()
{
printf("On test : ");
if (test()) printf("VRAI\n");
else printf("FAUX\n");
}
comme un carré est toujours plus petit qu'un cube si c'est plus grand que 10 je dois jamais sortir du while et ca doit tourné en rond. c'est ce que ça fait avec clang en mode debug mais si je demande un O3 alors ça plante plus et ça m'affiche vrai.
il y a eu un dépliage (la boucle se fait sur eax qui va de 5 en 5)
la boucle extérieure a sauté.
et de toutes façon en retourne 1
Ca ressemble a une optimisation un peu agressive, qui a un résultat observable équivalent si ça ne boucle pas. C'est à dire que le compilateur laisse tomber les comportements qui bouclent sans produire de résultat visible.
Idées :
les instructions de la boucle for ne peuvent que boucler indéfiniment, ou retourner 1.
la boucle while(true) fait toujours la même chose : boucler ou retourner 1. On peut "donc" ne la faire qu'une seule fois.
dommage qu'il n'ait pas poussé plus loin : la fonction aurait pu juste retourner 1
Si on bidouille un peu la fonction elle revient à un comportement conforme. Par exemple
si on faire un return i au lieu de return 1.
si on ajoute un printf("...") à côté du return
parce que là, l' "optimisation" ne peut plus se faire.
----
il faut quand même dire qu'un code de la forme
while (true) {
...
}
return qqchose;
et qui ne contient pas de break dans la boucle, ce n'est certainement pas la meilleure façon d'exprimer quelque chose de sensé.
Certes, il fait quelque chose quand il s'exécute, mais bon...
alors c'est un bug parceque le comportement est pas le même avec ou sans O3, on est bien d'accord? et la version bug c'est celle du O3 parceque en plus le bug saute si on rajoute un printf ?
Entièrement d'accord avec @michelbillaud. C'est ce qu'on appelle une optimisation agressive, tellement agressive qu'elle en produit un exécutable faux.
Donc oui, je dirai que c'est bel un bug clang. Quelle version de clang as-tu ? J'ai réussi à reproduire le souci avec clang 11.1.0 x86_64-pc-linux-gnu et la gnu libc.
Tout comme Michel, j'imagine que l'optimisation est :
on part du principe que le code fourni par le dèv est correct (erreur de la part de clang) ;
il remarque qu'il y a une boucle infinie et qu'on ne peut en sortir que par le return 1, il n'y a aucun autre moyen.
avec ça il se dit que comme le code est correct, il sort de la boucle et comme sortir de la boucle renvoie 1 c'est ce qu'il fait. Il doit le faire également parce que ton code n'a aucun effet de bord, ce qui expliquerait que rajouter un printf modifie l'optimisation, et que c'est une fonction constante = quel que soit l'état du programme il renverra toujours la même valeur, 1 en l'occurence.
Mais bon, il faudrait vérifier tout mes dires … c'est à vue de nez.
Quand on commence une phrase par "on est bien d'accord que", c'est une provocation au chipotage
ça dépend de ce qu'on appelle un bug.
Est-ce que le programme marche plus mal dans un cas que dans l'autre ? :-)
C'est clairement écrit un peu partout dans la doc que, quand on met des options d'optimisation , ça peut s'éloigner de ce qui est écrit dans le standard "that may violate strict compliance with language standards"
Le problème, c'est pas gcc ou clang, c'est que le langage C est mal foutu, et que c'est très difficile de s'assurer qu'une optimisation "avantageuse" est vraiment valide dans tous les cas (problème d'aliasing sur les paramètres, par exemple, quand deux pointeurs désignent en fait la même chose). Si on veut être sûr, on n'optimise pas ou presque (c'est le cas de la compilation du noyau linux, par exemple).
The upstream Linux kernel developers have come out against a proposal to begin using the "-O3" optimization level when compiling the open-source code-base with the GCC 10 compiler or newer.
Il s'y ajoute le fait que le code généré avec -O3 n'est parfois plus lent et plus gros que celui de -O2.
Michel … le code généré est clairement faux … il n'est pas plus gros ou plus lent (quoi que je n'ai pas regardé de près), il est faux si tu l'optimises en -O3 ou -O2 ou -O ou -Os)et correct sinon …
Et tu ne pas dire qu'un compilo qui transforme une boucle infinie en un programme qui termine est bugué …
Imagine le même code mais avec une vérification d'une conjecture quelconque, genre goldbach … le programme peut-être correct, le code généré ne le sera pas.
doit donner après compilation un exécutable qui fait une boucle infinie. Cela me semble plus que normal, non ?
Si l'exécutable ne boucle pas indéfiniment, c'est qu'il y a un problème lors de la compilation car l'exécutable produit n'est pas conforme au code source. Point, ça ne va pas plus loin que ça.
D'ailleurs, sans vouloir pousser le bouchon très loin, le code proposé par le PO est un code qui teste s'il existe un entier compris entre 10 et 99 inclus dont le carré est plus grand que le cube. Clairement le code produit avec optimisation est faux car l'exécutable finit par imprimer VRAI …
Si pour toi ce n'est pas un bug, je ne vois pas ce que c'est. Où alors il faut que tu m'expliques ce qu'est un bug pour toi.
Voir plus haut, les "optimisations" faites par les compilateurs ne préservent malheureusement pas la sémantique. Et sont donc, pour la plupart, fausses (parce qu'elles font l'impasse sur des cas plus ou moins tordus).
Pas la peine de me le reprocher, j'y suis pour rien !
Question : est ce que supprimer
for (int i=1; i !=0 ; i++)
for ( int j = 1; j != 0; j++)
continue;
Donne un programme équivalent, ou pas ?
Ps: non le programme ne teste pas si il existe un entier dans l'intervalle tel que. Ca serait une procedure de décision qui répond vrai ou faux au bout d'un temps fini. Là c'est une semi-decision, qui répond dans un cas, et boucle indéfiniment sinon.
rholala on se calme les gars. ici ou sur un autre site on me dit que mon code c'est de la merde mais non c'est un exemple minimaliste qui fait le même bug qu'un code plus gros que je vais pas mettre ici parceque il est trop gros.
mais même si le code est mal foutu, le programme est pire parceque il fait pas ce que le code demande de faire. clang a un probleme et je me dis clang c'est de la merde si on utilise O3. si j'ai bien compris ce que michel dit.
gcc c'est mieux dans ce cas 3et je suis d'accort avec fvirtman et whitecrow.
Je crois bien que tout le monde est d'accord en fait. Le code généré par clang -O3 sur cet exemple n'est pas correct. Ce qui est intéressant c'est de comprendre pourquoi il a été produit, c'est a dire quelles "recettes d'optimisation" ont été utilisées (indûment) pour produire ça.
Si ça peut te rassurer (?), gcc a aussi son lot de problèmes (= bugs) avec les optimisations. On n'est pas à l'abri de problèmes du même genre, sur d'autres exemples.
Ici ça se révèle sur un exemple "retourner une constante ou boucler" qui est un peu spécial, dans le sens où on considère généralement que du code qui boucle sans rien faire d'utile (de visible) ou qui fait plusieurs fois la même chose peut être "simplifié" sans dommage. Hypothèse qui est fausse dans la programmation bas niveau, par exemple on peut avoir de bonnes raisons de lire plusieurs fois de suite la même adresse en mémoire, parce qu'elle correspond en fait à un dispositif périphérique "mappé" en mémoire.
Autrement dit, on a tort de croire que les adresses, ça désigne de la mémoire.
Voir plus haut les fortes réticences (NIET !) pour changer le niveau d'optimisation dans le noyau Linux.
Tu t'égares Michel, même si on est à deux doigts de te voir dire que c'est effectivement un bug.
Quel que soit le code source, la compilation doit donner un programme conforme. C'est la tâche principale du compilateur.
michelbillaud a écrit:
Voir plus haut, les "optimisations" faites par les compilateurs ne préservent malheureusement pas la sémantique. Et sont donc, pour la plupart, fausses (parce qu'elles font l'impasse sur des cas plus ou moins tordus).
Pas la peine de me le reprocher, j'y suis pour rien !
Question : est ce que supprimer
for (int i=1; i !=0 ; i++)
for ( int j = 1; j != 0; j++)
continue;
Donne un programme équivalent, ou pas ?
Est-ce que le fait de supprimer un while(1) qui boucle indéfiniment donne un programme équivalent ?
euh … non.
michelbillaud a écrit:
Ps: non le programme ne teste pas si il existe un entier dans l'intervalle tel que. Ca serait une procedure de decision qui repond vrai ou faux au bout d'un temps fini. Là cest une semi-decision, qui repond dans un cas, et boucle indefiniment sinon.
- Edité par michelbillaud il y a environ 7 heures
Et le programme répond vrai immédiatement, donc bien que le code source soit correct le programme généré ne l'est pas
Je suppose qu'on clairement peut appeler ça un bug.
Stp, fais pas le pénible. J'ai toujours dit que le code était incorrect.
et qu'il est écrit partout dans la documentation que les optimisations "peuvent ne pas respecter le standard du langage". C'est à dire produire du code incorrect. C'est à dire que les compilateurs sont - quelle surprise - à peu près tous buggés.
Il se trouve que les programmeurs C prennent souvent le risque d'activer quand même ces optimisations. Ils ne sont pas à ça près, il faut croire.
Le fragment de code que je donnais (double boucle) ne boucle pas indéfiniment, il s'arrête quand les entiers ont fait le tour. Il prend très longtemps, par contre (2^64 tours pour des entiers sur 32 bits). ça fait une boucle d'attente assez longue. Est-ce que la supprimer pose un problème ? C'est un souci dans l'embarqué, quand on fait - idée discutable - un délai par une boucle. Si on supprime la boucle, il n'y a plus de délai, donc le programme ne fonctionne plus de la même façon. Est-ce souhaitable ? Est-ce normal ? Est-ce la faute au compilateur si le comportement attendu est supprimé par le compilateur ? Si un compilateur dérécursive une fonction alors que je l'ai écrit pour qu'il explose la pile dans certains cas, est-ce anormal ?
Je parlais de semi-décision parce que les gens qui font les compilateurs (et les optimisations) pensent parfois que le rôle d'une fonction C est de retourner forcément quelque chose au bout d'un certain temps. Ce qui explique certains bugs d'optimisation. Expliquer, c'est pas dire que c'est souhaitable, c'est juste constater que ça arrive, et qu'il y a des raisons.
La leçon à retenir est sans doutes qu'il faut être très méfiant sur les optimisations et les traiter comme s'il s'agissait d'une refactorisation de code.
Si je dois activer les optimisations, je ne le fais qu'une fois le programme au point et seulement pour de très bonnes raisons liées à la criticité du temps d'exécution et à un gain significatif que l'on peut mesurer. Ces cas sont, au final, très rares. Si le temps consommé par le processeur est à ce point précieux qu'il faut surveiller cette consommation à mesure que l'on développe, il faudrait que le cycle de développement comprenne l'exécution des tests unitaires avec une compilation sans optimisations et une compilation avec. C'est une galère supplémentaire.
Dans tous les cas, on ne devrait se risquer à introduire les optimisations qu'avec une batterie de tests unitaires permettant de vérifier que le programme continue de faire ce qu'il et sensé faire en cas de changements de niveau d'optimisations. Si l'on développe en TDD, c'est quelque chose qui vient naturellement, car les tests sont exécutés à chaque ajout de code ou refactorisation. Il faut juste envisager l'ajout de l'option d'optimisation comme un changement au code (objet), pour lequel il faut vérifier l'absence de régressions ou effets de bord indésirables tout comme si une refactorisation de code source était faite par un collègue "optimiseur" et dont les changements doivent passer les tests unitaires, comme toute refactorisation (ce que fait le compilateur au niveau de la production du code objet lorsqu'il utilise ses heuristiques pour produire une "optimisation" supposée "intelligente").
Stp, fais pas le pénible. J'ai toujours dit que le code était incorrect.
[...]
Le code n'est pas incorrect. C'est l'exécutable produit qui est incorrect. Que le code soit, selon ton propre jugement, correct ou non l'exécutable doit produit ce que le code décrit. Ce n'est pas le cas. Et ce qu'on demande en premier et qui est extrêmement important est qu'un compilateur produise ce qu'on lui demande (consciemment ou pas). Ce n'est pas le cas.
C'est pourquoi j'appelle ce comportement un bug de clang.
J'ai souvent encensé clang pour ses capacités d'optimisations qui vont souvent au-delà de celles de gcc. Mais sur le coup, c'est un coup à la confiance que je donne à clang sur sa capacité à être un compilateur digne de confiance (ok, j'exagère mais à peine).
Finalement je ne donne pas tort à Dlks quand il dit
Dlks a écrit:
La leçon à retenir est sans doutes qu'il faut être très méfiant sur les optimisations et les traiter comme s'il s'agissait d'une refactorisation de code. [...]
Et malheureusement c'est dommage de ne pas pouvoir faire une confiance aveugle en un simple -O.
Après le plus simple serait sans doute de demander directement des explications à clang … remplir un bug report …
Le PO pourra sans doute s'en charger puisque la découverte lui revient.
@whitecrow Jusqu'ici tu avais bien compris, avec le contexte, quand l'expression "le code" apparaissait, si il s'agissait du code source ou, le plus souvent en fait, du code généré par le compilateur. Surtout assaisonné du mot "incorrect".
Et tu étais parfaitement d'accord. En plus.
Qu'est-ce qui t'est arrivé depuis ? Tu as été optimisé ? :-)
Récemment, j'ai corrigé quelques fautes de frappes, accents qui manquaient etc quand je suis passé de la tablette sur l'ordinateur, dans un esprit du respect du lecteur, qui n'est pas obligé de se bouffer du français incorrect quand on peut arranger ça. On a ses manies. Je ne m'amuse pas à réécrire l'histoire.
--
Après, si on cherche un peu, on trouve dans le standard des choses qui peuvent faire douter.
C'est la combinaison du for (qui ne pourrait retourner que la valeur 1 ou se terminer - c'est là que la supposition intervient - sans avoir rien fait d'utile) et de la boucle infinie autour d'une instruction qui se termine mais n'aura rien fait d'utile, qui conduit à cette suprenante et douteuse optimisation : bon les gars, soit votre programme se barre en choucroute, soit il retourne forcément 1. Alors on va retourner 1, parce que sinon votre programme ne sert à rien.
Y a une logique, quand même. Avant d'aller pousser de hauts cris chez clang que leur machin est tout pété, je me méfierai :-) Apparemment le standard C n'exclue pas qu'on puisse optimiser en virant des boucles qui ne font rien d'utile, y compris si elles sont infinies (cf la note 157).
On pourrait mettre tout le monde d'accord en décrétant que le comportement de code qui fait une boucle infinie sans résultats observables est indéfini.
Le hic, c'est que, pour des raisons bien connues, ça ne peut pas être détecté par des outils dans le cas général. Il ne faut pas sous-estimer le poids des marchands d'outils dans la définition des normes et standards.
ah ok ok … certainement ce dont on parlait dès le départ non ?
White Crow a écrit:
[...] Tout comme Michel, j'imagine que l'optimisation est :
on part du principe que le code fourni par le dèv est correct (erreur de la part de clang) ;
il remarque qu'il y a une boucle infinie et qu'on ne peut en sortir que par le return 1, il n'y a aucun autre moyen.
avec ça il se dit que comme le code est correct, il sort de la boucle et comme sortir de la boucle renvoie 1 c'est ce qu'il fait. Il doit le faire également parce que ton code n'a aucun effet de bord, ce qui expliquerait que rajouter un printf modifie l'optimisation, et que c'est une fonction constante = quel que soit l'état du programme il renverra toujours la même valeur, 1 en l'occurence.
Mais bon, il faudrait vérifier tout mes dires … c'est à vue de nez.
Bon à la vue de la norme c'est toujours un bug, y compris pour le for interne car le return 1 n'est pas la seule manière de sortir du for (de considérer que la boucle termine). Et cette section ne concerne absolument pas le while ⇒ même optimisé le code doit boucler indéfiniment.
Et le plus simple pour mettre tout le monde d'accord est de résoudre le bug … l'optimiseur n'a pas à virer ce qu'il vire. Comme tu parles du problème de l'arrêt c'est clair que clang l'approche hyper mal …
michelbillaud a écrit:
[...] --- pendant qu'on y est, si je peux me permettre d'éditer ce message, le source suivant
void loop(void) { loop(); }
int main() {
loop();
}
quand il est compilé avec -O3
plante avec gcc (8.3.0) - par débordement de la pile.
se termine avec clang (7.0.1)
qui n'ont pas la même notion de "code inutile qu'on peut légalement supprimer".
PS: gcc qui est de mauvaise humeur n'a même pas été foutu de dérécursiver loop. c'est bien la peine de lui demander d'optimiser...
je retiens ça : gcc a un comportement constant et cohérent, clang non. clang a un bug qui n'est pas un bug mais qui fait ièch parceque c'est un bug quand même. la plupart d'entre vous défendes le compilo en disant que mon code est de la merde mais c'est qu'un code démo. les autres sont moins fermé
The compiler is given considerable freedom in how it implements the C program, but its output must have the same externally visible behavior that the program would have when interpreted by the “C abstract machine” that is described in the standard. Many knowledgeable people (including me) read this as saying that the termination behavior of a program must not be changed. Obviously some compiler writers disagree, or else don’t believe that it matters. The fact that reasonable people disagree on the interpretation would seem to indicate that the C standard is flawed.
Le premier point, un peu rigolo, est qu'au final nous sommes tous les même, ici ou ailleurs. Les mêmes «oppositions» se constatent dans cette discussion que dans celle des commentaires de l'article que tu proposes ou de l'article qui lui a donné naissance : C Compilers Disprove Fermat’s Last Theorem. Pourtant 11 ans nous en séparent. Il y a deux camps. Ceux des «si tu proposes un code débile tu auras une sortie carrément débile si tu lui demande de faire des trucs intelligents dessus» et ceux des «le compilo a visiblement changé le comportement observable d'un programme C conforme, ce qui est interdit». Je me place clairement dans le second.
Il y a cependant un truc que je ne comprends pas et que je devrais creuser si le temps le permet. Dans l'article on peut lire :
«Hall of Shame These C compilers known to not preserve termination properties of code: Sun CC 5.10, Intel CC 11.1, LLVM 2.7, Open64 4.2.3, and Microsoft Visual C 2008 and 2010. The LLVM developers consider this behavior a bug and have since fixed it. As far as I know, the other compiler vendors have no plans to change the behavior.»
«This issue is fixed in LLVM 12 by the introduction of the mustprogress attribute and loop metadata, inference of willreturn and finally limitation of call DCE to willreturn functions.
There might still be some incorrect assumptions left over here and there, but the core issue is fixed now, and we should track remaining issues separately.»
Et comme l'intitulé du BR965 concerne C et Rust, j'ai réessayé le code du PO avec clang 12. Je n'ai constaté aucun changement par rapport à clang11, le bug apparaît toujours.
C'est à dire qu'il y une contradiction flagrante dans le standard (C11 au moins) entre
le principe selon lequel l'exécutable devrait fonctionner comme la "machine abstraite" (*)
"allow compiler transformations such as removal of empty loops even when termination cannot be proven".
Donc c'est délicat de dire "tout le monde est d'accord que c'est clairement incorrect". Faut rester prudent.
L'explication du "bug toujours non résolu" est peut être dans la phrase
> There might still be some incorrect assumptions left over here and there,
--
(*) surtout que ladite machine abstraite n'est définie formellement nulle part, pas plus que la traduction "standard" du source C en instructions de la machine abstraite. Ca reste très informel tout ça. Et donc ambigu.
Je ne dis pas que c'est incorrect, je dis que c'est un bug … bug, comme dans bug report … bug report qui a amené une correction de bug … apparemment imparfaite.
Tu admettras tout de même que dans l'exemple de ton article (enfin surtout celui qui a amené la création de celui-ci) montre que l'on peut, avec clang, produire des programmes faux à partir de source correctes, mais imparfaite. Ça c'est mon point de vue originel.
Mais surtout, il y a le problème du changement de comportement. Je m'explique, dans le standard en 5.1.2.3.1 :
«The semantic descriptions in this International Standard describe the behavior of an abstract machine in which issues of optimization are irrelevant.»
un peu plus loin en 5.1.2.3.6 :
«The least requirements on a conforming implementation are:
— Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.
— At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
— The input and output dynamics of interactive devices shall take place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.
This is the observable behavior of the program.»
L'optimisation dont nous parlons change le comportement observable du programme. Disons plutôt qu'il y a deux comportement observable pour le même code source … lequel des deux est correct ? lequel des deux ne l'est pas ? pour celui qui ne l'est pas quel en est la cause ?
Je pense que cette cause peut être appelée, comme dans le BR965 de LLVM, un bug ; que ce soit un cas ou l'autre.
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
Recueil de code C et C++ http://fvirtman.free.fr/recueil/index.html
Le Tout est souvent plus grand que la somme de ses parties.
Le Tout est souvent plus grand que la somme de ses parties.