IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Langage C++ Discussion :

Comportements différents dans main() et dans foo()


Sujet :

Langage C++

  1. #1
    Membre confirmé
    Profil pro
    Inscrit en
    Avril 2012
    Messages
    103
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Avril 2012
    Messages : 103
    Par défaut Comportements différents dans main() et dans foo()
    Bonjour

    Je n'ai jamais eu ce problème. Et sans doute vous non plus !
    Une copie du debugging vaudra mieux qu'un long discours.
    Nom : foo_bug.JPG
Affichages : 207
Taille : 103,7 Ko


    Comme on peut le constater, la première partie de main() est un copier/coller de foo().
    1) la première partie de main() fonctionne bien (voir la console)
    2) foo() dysfonctionne complètement. Le debugging montre que
    * pour x = 0 on a (x != 0) = true (cf b1) ce qui donne une boucle infinie à l'exécution.
    * pour j = 48 on a (j > 15) = false (cf b2) ce qui ne stoppe pas la boucle infinie.

    Je n'y comprends rien ! Toute idée est la bienvenue. Merci d'avance.

    PS : en général __builtin_clzll() n'est pas définie en 0. La variable 'j' est donc aléatoire pour x = 0 : la valeur '48' n'est donc pas reproductible.
    Il est d'ailleurs très probable que ce bug ne le soit pas non plus !

    Contexte : Windows 10 // Code::Blocs 25.03 // MinGW_w64 Compiler (-O3)

  2. #2
    Membre confirmé
    Profil pro
    Inscrit en
    Avril 2012
    Messages
    103
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Avril 2012
    Messages : 103
    Par défaut
    Bonjour

    Désolé d'être intervenu pour rien (un modérateur peut peut-être supprimer ce fil).

    Le coupable est le uint32_t devant foo() alors qu'il n'y a pas de return. Le programme tourne en attendant ce 'return' et a donc un comportement imprévisible. Le seul point positif de ceci est que face à un comportement invraissemblable il faut chercher l'erreur dans cette direction.

  3. #3
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 590
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 590
    Par défaut
    Bonsoir et merci pour le retour d'expérience.

    Il n'y a pas lieu de supprimer ici. Ce serait dommage de le faire car ton problème initial est proprement documenté et parce que la solution peut faire gagner beaucoup temps à ceux qui commettent le même oubli.

  4. #4
    Membre confirmé
    Profil pro
    Inscrit en
    Avril 2012
    Messages
    103
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Avril 2012
    Messages : 103
    Par défaut
    Merci Obsidian

    Que le programme tourne en rond : OK.
    Mais même en remplaçant b1 = (x != 0) par b1 = static_cast<bool>(x) on obtient toujours
    'b1 = 1' pour 'x = 0' . C'est quand même dur à avaler !

    PS : en examinant plus attentivement le 'Build messages' j'ai vu un warning signalant le problème.
    Est-ce qu'un warning est suffisant pour ce genre d'erreur ?

  5. #5
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 590
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 590
    Par défaut
    Bonjour,
    J'ai relu ton précédent message un peu plus attentivement. Il y a quand même quelque chose qui m'ennuie :

    Citation Envoyé par serge17 Voir le message
    Mais même en remplaçant b1 = (x != 0) par b1 = static_cast<bool>(x) on obtient toujours
    'b1 = 1' pour 'x = 0' . C'est quand même dur à avaler !
    Je lis :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    while (x != 0)
    {
        …
        b1 = (x != 0); cout<<"\nb1 = "<<b1;
        …
        …
    }
    b1 est définie dans la la boucle, qui est inféodée à la même condition. Cela signifie qu'elle sera définie et le message affiché qu'à condition que x soit déjà différente de zéro.

    Par ailleurs, tu mets des retours à la ligne en début de message mais pas à la fin. Donc il est possible que chaque message émis reste dans le tampon jusqu'à l'émission du suivant.

    Maintenant, ça ne devrait pas empêcher non plus ta boucle de se terminer car dans le cas d'un entier non signé << et <<= ne sont pas des UB dans le cas d'un débordement. En revanche, on ne sait pas à quoi sert cette fameuse fonction __builtin_clzll() ni même si c'est une fonction pure. Si toutefois x lui est passée par référence et que son nom signifie « clear and zero (long long) » et donc qu'à tout hasard, elle remette systématiquement x à zéro, alors effectivement ta boucle ne se terminerait jamais parce que cette remise à zéro serait toujours suivie d'un x <<= 1 juste avant la fin du bloc.

    UPDATE : __builtin_clzll() est une fonction builtin de GCC : https://gcc.gnu.org/onlinedocs/gcc/B...-Builtins.html

    Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
    En recopiant ta fonction et en l'exécutant sous GDB, j'aboutis à un SIGILL qui est en fait provoqué explicitement par une instruction x86 « ud2 », qui sert à émuler ce comportement, probablement pour respecter l'algo de l'instruction BSR dédiée à cela : https://www.felixcloutier.com/x86/bsr

  6. #6
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 590
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 590
    Par défaut
    Je viens de faire le test un peu plus loin :

    • Avec g++ -O0, j'obtiens le comportement décrit ci-dessus ;
    • Avec g++ -O3, j'entre dans une boucle infinie (UPDATE : avec -O1 à -O3, en réalité).


    Il s'agit donc bien d'un undefined behavour, et à très bas niveau.

    Voici le code désassemblé et commenté par mes soins de la fonction foo(), produit avec l'option -S de g++, puis vérifié avec gdb et objdump --disassemble :

    Code asm : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
     
     
    .L2:
            movl    $12, %edx          ; Longueur chaîne = 12
            movl    $.LC0, %esi        ; "\n x debut = "
            movl    $_ZSt4cout, %edi   ; cout
            call    _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
     
            movq    %rbx, %rsi         ; (RBX = x)
            movl    $_ZSt4cout, %edi   ; cout
            call    _ZNSo9_M_insertImEERSoT_
     
            movl    $7, %edx           ; Longueur chaîne = 7
            movl    $.LC1, %esi        ; "\n b1 = "
            movl    $_ZSt4cout, %edi   ; cout
            call    _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
     
            movl    $1, %esi           ; (int)1
            movl    $_ZSt4cout, %edi   ; cout
            call    _ZNSo9_M_insertIbEERSoT_
     
            bsrq    %rbx, %rbp         ; Instruction BSR, pour __builtin_clzll(). n = rang du premier bit à 1
             --
            movl    $7, %edx           ; Longueur chaîne = 7
            movl    $.LC2, %esi        ; "\n b2 = "
            movl    $_ZSt4cout, %edi   ; cout
             --
            xorq    $63, %rbp          ; XOR 63 sur 7 bits => « -n-1 » => Nombre de bits à zéro à gauche
             --
            call    _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
     
            xorl    %esi, %esi         ; (int)0
            movl    $_ZSt4cout, %edi   ; cout
            call    _ZNSo9_M_insertIbEERSoT_
     
            movl    %ebp, %ecx
             --
            movl    $8, %edx           ; Longueur chaîne = 8
            movl    $.LC3, %esi        ; "\n x fin "
             --
            salq    %cl, %rbx          ; x <<= j
             --
            movl    $_ZSt4cout, %edi   ; cout
            call    _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
     
            addq    %rbx, %rbx         ; x = x + x  =>  x *=2  =>  x <<= 1
     
            movl    $_ZSt4cout, %edi   ; cout
            movq    %rbx, %rsi         ; (RBX = x)
            call    _ZNSo9_M_insertImEERSoT_
     
            movl    $8, %edx           ; Longueur chaîne = 8
            movl    $.LC4, %esi        ; "\n ------"
            movq    %rax, %rdi
            call    _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
     
            jmp     .L2

    On remarque que ce code ne contient aucune comparaison ni test de nullité, et que l'unique saut est le jump en fin de procédure qui remonte au début. On constate également deux choses :

    • Tout se passe donc comme si le compilateur, ici, faisait abstraction du format de x. En conséquence, comme tu n'effectues que des décalages vers la gauche, x ne peut arithmétiquement jamais nul s'il n'est pas explicitement borné. Le compilateur fait donc l'économie du contrôle de la valeur de x en début de boucle ;
    • b1 et b2 sont considérées comme des constantes, ce qui confirme que x est considéré comme n'étant jamais nul et qui implique que le compilateur « sait » que j ne sera jamais supérieur à q. Pour la même raison, la ligne suivante est totalement ignorée et n'apparaît jamais dans le code compilé.


    Pour b2, cela se démontre : si x démarre à 1<<48 donc avec 7 bits nuls à gauche, que x n'est jamais nul (conditionné par la boucle) et qu'on ne fait que le déplacer vers la gauche, alors tant qu'il restera des bits à 1 dedans, il y aura forcément moins de 7 bits nuls restants à chaque tour (et a fortiori moins de 15).

    Pour le reste, il est difficile de dire exactement dans quelle mesure il s'agit ou non d'un bug de GCC. D'un côté, conditionner le contrôle d'une boucle au débordement de la variable peut être considéré a priori comme un UB mais de l'autre, l'application d'un modulo sur le MAX_SIZE+1 du type à condition qu'il soit non signé est défini explicitement aussi bien dans C++11 que dans C11.

  7. #7
    Membre confirmé
    Profil pro
    Inscrit en
    Avril 2012
    Messages
    103
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Avril 2012
    Messages : 103
    Par défaut
    Waouh ! Gros boulot. Je vais lire tout ça attentivement.

  8. #8
    Membre Expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    776
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 776
    Par défaut
    Citation Envoyé par Obsidian Voir le message
    Pour le reste, il est difficile de dire exactement dans quelle mesure il s'agit ou non d'un bug de GCC. D'un côté, conditionner le contrôle d'une boucle au débordement de la variable peut être considéré a priori comme un UB mais de l'autre, l'application d'un modulo sur le MAX_SIZE+1 du type à condition qu'il soit non signé est défini explicitement aussi bien dans C++11 que dans C11.
    Les débordements sur les non signés sont parfaitement définie, l'UB n'est pas là. Serge le dit lui même dans son second message: il manque un return dans la fonction. À partir de là, le compilateur peut bien faire ce qu'il veut.

    Je ne comprends toujours pas pourquoi le gens ne compilent pas avec les avertissements activés... -Wall -Wextra est le minimum vital.

  9. #9
    Membre confirmé
    Profil pro
    Inscrit en
    Avril 2012
    Messages
    103
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Avril 2012
    Messages : 103
    Par défaut
    Merci d'intervenir Jo_link_noir

    -Wfatal-errors
    -Wextra
    -Wall

    sont mes options de compilation.

    Comme je le disais à Obsidian, il y a un warning indiquant le problème.
    "Bien sûr" je ne l'ai pas remarqué ; je n'ai vu que le '0 error'.

    Un warning est-il suffisant pour ce genre d'erreur ? Ca va coincer à tous les coups, non ?

  10. #10
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 590
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 590
    Par défaut
    (EDIT : crosspost)

    Citation Envoyé par jo_link_noir Voir le message
    Les débordements sur les non signés sont parfaitement définie, l'UB n'est pas là. Serge le dit lui même dans son second message: il manque un return dans la fonction. À partir de là, le compilateur peut bien faire ce qu'il veut. Je ne comprends toujours pas pourquoi le gens ne compilent pas avec les avertissements activés...
    Parce qu'ici, le problème initial (le return manquant) a été identifié et résolu dès le départ par le primo-postant. Il a ensuite ajouté :

    Citation Envoyé par serge17 Voir le message
    PS : en examinant plus attentivement le 'Build messages' j'ai vu un warning signalant le problème.
    Est-ce qu'un warning est suffisant pour ce genre d'erreur ?

    … et c'est à partir de là que l'on s'est mis à investiguer. D'abord parce que même si la sortie de fonction sans return est indéterminée, ça ne devrait pas remonter jusqu'à perturber la boucle en principe, ensuite parce que comme indiqué, les débordements sont définis sur les non-signés uniquement (c'est le cas ici), parce que ça dépendait aussi du niveau d'optimisation et parce qu'on se demandait aussi si c'était lié ou non à l'usage de __builtin_clzll().

    Mais surtout, il était important de savoir si c'était réellement un bug ou non parce que dans le premier cas, il pourrait alors se produire en conditions légitimes également.

    Il est peu probable que ce soit le cas, toutefois, parce que le même programme réduit à sa portion congrue et sans l'appel à la fonction __builtin se comporte quand même de la même façon :

    Code C++ : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <cstdint>
    #include <iostream>
     
    int foo(void)
    {
        uint64_t x = 1LL << 48;
     
        while (x != 0)
        {
    	std::cout << std::hex << x << std::endl;
    	x <<= 1;
        }
    }
     
    int main(void)
    {
        foo();
        std::cout << "Done.\n";
        return 0;
    }

    et également parce qu'en optimisation -O0, GCC insère explicitement une instruction « ud2 » dans le code. C'est donc tout-à-fait volontaire et il est donc admissible en ce sens qu'elle soit « optimized out » par la suite. Le choix d'entrer dans une boucle infinie plutôt que de la laisser déboucher sur une instruction interdite reste discutable : que serait-il passé si la boucle remplissait un fichier ou envoyait des données sur le réseau, par exemple ?

  11. #11
    Membre Expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    776
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 776
    Par défaut
    Citation Envoyé par Obsidian Voir le message
    D'abord parce que même si la sortie de fonction sans return est indéterminée, ça ne devrait pas remonter jusqu'à perturber la boucle en principe
    Si, c'est ce que permettent les UB. Et un UB dans du code optimiser peut avoir des comportements très surprenant parce que l'optimiseur est libre de l’interpréter comme il veut.

    Ici, puisqu'il n'y a pas de return, la fonction peut ne jamais finir (ce qui est aussi UB au passage avant C++26). Si la fonction ne se termine pas, c'est que la boucle est infinie. Si elle est infinie, pas besoin de condition.

    Un UB n'est pas juste local à la ligne, il impacte toute la chaîne d'optimisation. Chercher un bug de génération de code en présence d'UB est peine perdue ; le compilateur est libre de faire ce qu'il veut.

    Citation Envoyé par serge17 Voir le message
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    -Wfatal-errors
    -Wextra
    -Wall
    sont mes options de compilation.
    [...]
    Un warning est-il suffisant pour ce genre d'erreur ? Ca va coincer à tous les coups, non ?
    Je ne recommande pas -Wfatal-errors, avoir les erreurs au compte goutte est vite pénible. Au pire, il y a -fmax-errors=n pour ne pas en avoir trop à la fois.

    Sinon, les compilateurs font la distinction entre ce qui relève de la syntaxe et des règles du langage de ce qui relève plutôt de la bonne hygiène du code.
    Les erreurs sont dans la première catégorie: la norme interdit tel truc, alors le programme ne compile pas.
    Les warnings sont tous ce qui est probablement un bug, mais que la norme n'interdit pas.

    Certains avertissements sont plus critiques que d'autres, mais cela reste des avertissements. Par contre, on peut demander à ce qu'ils soient transformés en erreur. C'est personnellement ce que je fais pour -Wreturn-type avec -Werror=return-type. Il y a aussi -Werror pour que tous les avertissements soient des erreurs, mais je ne la conseille pas.

  12. #12
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 590
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 590
    Par défaut
    Citation Envoyé par jo_link_noir Voir le message
    Si, c'est ce que permettent les UB. Et un UB dans du code optimiser peut avoir des comportements très surprenant parce que l'optimiseur est libre de l’interpréter comme il veut.
    On le sait bien également mais là encore, on peut s'arrêter au commentaire #2. Certes le compilateur peut bien chanter la marseillaise ou repeindre la pièce en rose si ça l'amuse dès lors qu'il est établi que l'on s'est mis en situation d'UB, mais ce n'est pas non plus un blanc seing qu'on lui accorde pour se détendre. Les cas indéfinis sont les choses qui sont volontairement laissées hors specs justement pour éviter d'avoir à poser des contraintes inutiles.

    Un comportement indéfini dans le cas d'un return oublié, même si en théorie cela peut être n'importe quoi, pourrait raisonnablement être soit une segfault, soit le renvoi d'une valeur aléatoire, soit la valeur de la dernière expression évaluée en date au sein de la fonction (comme cela se fait dans certains langages), soit la valeur courante de RAX, soit à la limite une terminaison prématurée avec abort().

    Il n'y a rien qui puisse laisser penser de prime abord qu'un return oublié puisse causer ce genre de comportement et justement, avant même de penser à un bug du compilo (c'est possible mais il faut avoir des arguments solides pour le soutenir), on pensait surtout à un bug du programme lui-même. C'est précisément parce qu'on ne le trouvait pas que le primo-postant a très judicieusement demandé si cela ne devrait pas faire l'objet d'un warning un peu plus explicite et qu'on s'est mis à essayer d'en isoler la cause.

    Ici, puisqu'il n'y a pas de return, la fonction peut ne jamais finir (ce qui est aussi UB au passage avant C++26). Si la fonction ne se termine pas, c'est que la boucle est infinie. Si elle est infinie, pas besoin de condition.
    C'est avec cela que je ne suis pas d'accord. On ne peut pas faire ce raccourci.

    D'abord parce qu'il semble avoir été fait a posteriori pour coller à la situation, ensuite parce qu'on ne peut pas changer le prédicat de la boucle dans des proportions aussi importantes en fonction de ce qui se passe après. Que se passerait-il s'il y avait deux boucles successives ou pas de boucle du tout au sein de cette fonction ? Que se passerait-il également s'il y avait des instructions supplémentaires après la boucle avant la fin du corps de la fonction (spoil : je viens d'essayer : la boucle se termine à nouveau normalement, les instructions en question sont honorées et le programme se termine en segfault avant de pouvoir revenir vers main). Surtout que, là encore, il n'y a pas de comportement indéterminé dans le corps de la boucle ni dans sa condition de contrôle. Donc on ne peut même pas considérer que le travail effectué en son sein ait pu mettre l'environnement dans un état instable comme dans le cas d'un dépassement de tampon, par exemple.

    Or, si on regarde le code compilé, contrairement à celui compilé avec -O0, il n'y a plus du tout de comparaison de valeurs ni même de saut conditionnel (uniquement un jmp). Le compilateur l'a donc fait exprès et il ne s'agit pas en l'état d'un code valide dans les conditions normales d'exploitation et qui cesserait de l'être parce qu'on en est sorti.

    En écrivant ce message, j'ai fini par conclure qu'en réalité, le saut de sortie de boucle avait dû en fait être placé en milieu de boucle à la suite d'une opération arithmétique sur x (l'équivalent d'un break, donc) et qu'il y a de fortes chances que ce soit justement parce que la sortie n'existe pas que ces directives en particulier ont été ignorées et exclues du code final.

    Un test de compilation avec et sans return suivi d'une comparaison avec vimdiff semble confirmer cette hypothèse (en lignes 9 et 10 ci-dessous) :

    Code ASM : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
     
              movsbl  67(%rbx), %esi                                                                         |          movsbl  67(%rbx), %esi                                                                                                                                                                                   
      .L6:                                                                                                   |  .L6:
              movq    %rbp, %rdi                                                                             |          movq    %rbp, %rdi
              addq    %r12, %r12                                                                             |          addq    %r12, %r12                                                                                                                                                                                       
              call    _ZNSo3putEc                                                                            |          call    _ZNSo3putEc
              movq    %rax, %rdi                                                                             |          movq    %rax, %rdi                                                                                                                                                                                       
              call    _ZNSo5flushEv                                                                          |          call    _ZNSo5flushEv
      -------------------------------------------------------------------------------------------------------|          subl    $1, %r13d                                                                                                                                                                                        
      -------------------------------------------------------------------------------------------------------|          je      .L13                                                                                                                                                                                             
      .L7:                                                                                                   |  .L7:
              movq    _ZSt4cout(%rip), %rax                                                                  |          movq    _ZSt4cout(%rip), %rax
              movq    %r12, %rsi                                                                             |          movq    %r12, %rsi                                                                                                                                                                                       
              movl    $_ZSt4cout, %edi                                                                       |          movl    $_ZSt4cout, %edi                                                                                                                                                                                 
              movq    -24(%rax), %rdx                                                                        |          movq    -24(%rax), %rdx                                                                                                                                                                                  
              movl    _ZSt4cout+24(%rdx), %eax                                                               |          movl    _ZSt4cout+24(%rdx), %eax                                                                                                                                                                         
    + +--  3 lignes : andl $-75, %eax------------------------------------------------------------------------|+ +--  3 lignes : andl $-75, %eax----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
              call    _ZNSo9_M_insertImEERSoT_                                                               |          call    _ZNSo9_M_insertImEERSoT_
              movq    %rax, %rbp                                                                             |          movq    %rax, %rbp
              movq    (%rax), %rax                                                                           |          movq    (%rax), %rax
              movq    -24(%rax), %rax                                                                        |          movq    -24(%rax), %rax                                                                                                                                                                                  
              movq    240(%rbp,%rax), %rbx                                                                   |          movq    240(%rbp,%rax), %rbx
              testq   %rbx, %rbx                                                                             |          testq   %rbx, %rbx
              je      .L10                                                                                   |          je      .L11                                                                                                                                                                                             
              cmpb    $0, 56(%rbx)                                                                           |          cmpb    $0, 56(%rbx)
              jne     .L12                                                                                   |          jne     .L14                                                                                                                                                                                             
              movq    %rbx, %rdi                                                                             |          movq    %rbx, %rdi                                                                                                                                                                                       
              call    _ZNKSt5ctypeIcE13_M_widen_initEv                                                       |          call    _ZNKSt5ctypeIcE13_M_widen_initEv                                                                                                                                                                 
              movq    (%rbx), %rax                                                                           |          movq    (%rbx), %rax                                                                                                                                                                                     
              movl    $10, %esi                                                                              |          movl    $10, %esi                                                                                                                                                                                        
              movq    48(%rax), %rax                                                                         |          movq    48(%rax), %rax                                                                                                                                                                                   
              cmpq    $_ZNKSt5ctypeIcE8do_widenEc, %rax                                                      |          cmpq    $_ZNKSt5ctypeIcE8do_widenEc, %rax
              je      .L6                                                                                    |          je      .L6
              movq    %rbx, %rdi                                                                             |          movq    %rbx, %rdi
              call    *%rax                                                                                  |          call    *%rax
              movsbl  %al, %esi                                                                              |          movsbl  %al, %esi
              jmp     .L6                                                                                    |          jmp     .L6
      -------------------------------------------------------------------------------------------------------|          .p2align 4,,10                                                                                                                                                                                           
      -------------------------------------------------------------------------------------------------------|          .p2align 3                                                                                                                                                                                               
      -------------------------------------------------------------------------------------------------------|  .L13:                                                                                                                                                                                                            
      -------------------------------------------------------------------------------------------------------|          addq    $8, %rsp                                                                                                                                                                                         
      -------------------------------------------------------------------------------------------------------|          xorl    %eax, %eax                                                                                                                                                                                       
      -------------------------------------------------------------------------------------------------------|          popq    %rbx                                                                                                                                                                                             
      -------------------------------------------------------------------------------------------------------|          popq    %rbp                                                                                                                                                                                             
      -------------------------------------------------------------------------------------------------------|          popq    %r12                                                                                                                                                                                             
      -------------------------------------------------------------------------------------------------------|          popq    %r13                                                                                                                                                                                             
      -------------------------------------------------------------------------------------------------------|          ret

  13. #13
    Membre Expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    776
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 776
    Par défaut
    Citation Envoyé par Obsidian Voir le message
    Les cas indéfinis sont les choses qui sont volontairement laissées hors specs justement pour éviter d'avoir à poser des contraintes inutiles.
    Et afin d'optimisation en considérant des choses qui ne sont pas toujours vrai. Comme x + 1 > x qui est toujours vrai pour un signé... Pour le compilateur. Cela permet d'utiliser une instruction plus rapide s'il existe une qui ne gère pas l'overflow.

    Citation Envoyé par Obsidian Voir le message
    Un comportement indéfini dans le cas d'un return oublié, même si en théorie cela peut être n'importe quoi, pourrait raisonnablement être soit une segfault, soit le renvoi d'une valeur aléatoire, soit la valeur de la dernière expression évaluée en date au sein de la fonction (comme cela se fait dans certains langages), soit la valeur courante de RAX, soit à la limite une terminaison prématurée avec abort().
    Ou autre. Je ne vois pas pourquoi tu veux un truc raisonnable sur un UB alors que le principe est de l'ignorer pour optimiser ou simplifier les implémentations.

    Ce que dit https://cppreference.com/cpp/language/ub:

    > undefined behavior - There are no restrictions on the behavior of the program.
    > [...]
    > Implementations are not required to diagnose undefined behavior (although many simple situations are diagnosed), and the compiled program is not required to do anything meaningful.

    Au passage, il y a plusieurs liens en bas de page sur le blog de LLVM (en 3 parties), de Microsoft et autres.


    Citation Envoyé par Obsidian Voir le message
    C'est précisément parce qu'on ne le trouvait pas que le primo-postant a très judicieusement demandé si cela ne devrait pas faire l'objet d'un warning un peu plus explicite et qu'on s'est mis à essayer d'en isoler la cause.
    Il y a déjà un warning dédié: -Wreturn-type que Serge n'a pas vu parce que le programme compile sans erreur.

    Citation Envoyé par Obsidian Voir le message
    C'est avec cela que je ne suis pas d'accord. On ne peut pas faire ce raccourci. D'abord parce qu'il semble avoir été fait a posteriori pour coller à la situation
    Zut, je suis démasqué .

    Citation Envoyé par Obsidian Voir le message
    ensuite parce qu'on ne peut pas changer le prédicat de la boucle dans des proportions aussi importantes en fonction de ce qui se passe après.
    [...]
    En écrivant ce message, j'ai fini par conclure qu'en réalité, le saut de sortie de boucle avait dû en fait être placé en milieu de boucle à la suite d'une opération arithmétique sur x (l'équivalent d'un break, donc) et qu'il y a de fortes chances que ce soit justement parce que la sortie n'existe pas que ces directives en particulier ont été ignorées et exclues du code final.
    Je ne sais pas si de ton point de vu le second paragraphe contredit le premier, mais passons.

    Quand le compilateur optimise, il va principalement supprimer ou fusionner des instructions en plus de les réordonner. Dans le cas d'UB, il a tendance à supprimer purement et simplement le code concerné. Après de multiple passe de suppression / fusion, l'ensemble n'est plus forcément cohérent par rapport au comportement attendue. Généralement les conditions sautent, donc des branche aussi et à la fin il ne reste plus grand chose. Le segfault que tu rencontres avec gcc et dû au fait que le jump se fait sur un label qui n'existe plus. Il ne faut pas grand chose pour que le comportement change totalement. Par exemple, remplacer std::cout par printf va faire une boucle infinie (dans ce code: https://godbolt.org/z/TcKbh98ar).

    On remarque que msvc ne veut pas compiler parce qu'il manque le return. Probablement le choix le plus sensé.

  14. #14
    Membre confirmé
    Profil pro
    Inscrit en
    Avril 2012
    Messages
    103
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Avril 2012
    Messages : 103
    Par défaut
    Merci à tous les deux pour votre implication. Je ne m'attendais pas à ce que mon petit problème (que je voulais même faire supprimer) déclenche une telle bataille d'experts. Bon, le terme "bataille" n'est peut-être pas approprié, mais je n'en est pas d'autre sous la main.
    J'ai appris pas mal de chose en vous lisant (et j'en apprendrais encore sans doute d'autres en vous relisant !)

    Par exemple, je trouvais dommage que cette erreur soit seulement un warning : du coup j'ai ajouté l'option -Werror=return-type pour ne plus avoir ce genre de soucis.
    Comme le dis Jo_link-noir :
    On remarque que msvc ne veut pas compiler parce qu'il manque le return. Probablement le choix le plus sensé.
    De même, j'utilise -Wfatal-errors parce que je ne supportais pas d'avoir un flot invraisemblables d'erreurs qui en fait n'en étaient pas et qui s'évanouissaient dès que la première erreur était corrigée. Je ne connaissais pas -fmax-errors=n qui est sans doute un bon compromis (erreurs identiques à cause de copier/coller par exemple).

  15. #15
    Membre Expert

    Homme Profil pro
    Directeur de projet
    Inscrit en
    Mai 2013
    Messages
    1 820
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Directeur de projet
    Secteur : Service public

    Informations forums :
    Inscription : Mai 2013
    Messages : 1 820
    Par défaut
    Bonjour,

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void foo() {
       uint32_t j = 0, q = 15;                       // q = 15 => xmin = 0x0001 0000 0000 0000 = 1 << 48
       uint64_t x = 1;                               // ajouter ? uint64_t xmin = uint64_t(-1) >> q
       x <<= 48;                                     // x = 0x0001 0000 0000 0000 = xmin
       while(x != 0) {                               // ou while(x < xmin)
          cout << "\n x = " << x;                    // Ce n'est pas x début puisque dans la boucle
          // b1 supprimé car toujours vrai dans la boucle
          j = __builtin_clzll(x);                    // Nombre de 0 : 15 par construction 
          // b2 supprimé car double emploi avec le test suivant
          if(j > q) {
             cout << "\n erreur : j =" << j << " > " << q;
             return;
          }
          x <<= (j + 1);                             // Mange les 0 à gauche et le dernier bit à 1
       }
    }
    Je ne suis pas certain de savoir ce que ce code était censé faire.

    Mettons de côté l'incohérence d'avoir un type de retour sans avoir de retour explicite. On pourrait même supposé qu'à compilateur différent, voire à optimisation différente, on ait des manifestations différentes.

    J'ai donc un peu modifié le code sans en changer la logique (avec un peu de chance, je n'ai peut-être pas introduit d'erreurs supplémentaires ).

    Que fait-il ? Il compte les 0 à gauche, crie et s'arrête si plus de 15, sinon retire les 0 trouvés et le dernier 1 par un décalage puis réitère. Comme x est une puissance de 2, x n'a qu'un seul bit à 1 qui disparaît au premier passage. C'est donc une boucle d'un seul passage. Que les optimisations s'en aperçoivent et fassent disparaître le while est plutôt sain.

    Salutations
    Ever tried. Ever failed. No matter. Try Again. Fail again. Fail better. (Samuel Beckett)

  16. #16
    Modérateur
    Avatar de Obsidian
    Homme Profil pro
    Chercheur d'emploi
    Inscrit en
    Septembre 2007
    Messages
    7 590
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur d'emploi
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2007
    Messages : 7 590
    Par défaut
    Bonjour à tous,

    Citation Envoyé par jo_link_noir Voir le message
    Je ne sais pas si de ton point de vu le second paragraphe contredit le premier, mais passons.
    En fait non, justement et c'est ce que l'on cherchait à déterminer, ou en tout cas à savoir quelle approche GCC décide d'adopter dans ce genre de situation. Mais en fait, le cheminement semble assez simple a posteriori : indépendamment du return, l'optimisation à partir de -O1 transforme un :


    en :

    Code c++ : Sélectionner tout - Visualiser dans une fenêtre à part
    while (true) { if (!x) break; }

    … et c'est ensuite au moment de compiler l'instruction if (!x) break; elle-même que celle-ci passe à la trappe faute d'un point de sortie, probablement au moment où le warning est émis d'ailleurs et avant que le compilateur poursuive son travail. En ce sens, la condition de contrôle est bien « déplacée » mais pas « remplacée ». Le while(true) correspond à une boucle inconditionnelle parce qu'il n'existe pas de mot-clé dédié pour cela (à titre de comparaison, on avait DO … LOOP en BASIC 128 et 512).

    Citation Envoyé par serge17 Voir le message
    Merci à tous les deux pour votre implication. Je ne m'attendais pas à ce que mon petit problème (que je voulais même faire supprimer) déclenche une telle bataille d'experts. Bon, le terme "bataille" n'est peut-être pas approprié, mais je n'en est pas d'autre sous la main.
    Non, non, nulle bataille ici. :-) Les compétences de Jo_Link_Noir ne sont plus à démontrer ici et c'est toujours un plaisir de lire ses interventions. J'étais plutôt en train de défendre un point de vue, surtout

    Par exemple, je trouvais dommage que cette erreur soit seulement un warning : du coup j'ai ajouté l'option -Werror=return-type pour ne plus avoir ce genre de soucis.

    Comme le dis Jo_link-noir :
    De même, j'utilise -Wfatal-errors parce que je ne supportais pas d'avoir un flot invraisemblables d'erreurs qui en fait n'en étaient pas et qui s'évanouissaient dès que la première erreur était corrigée. Je ne connaissais pas -fmax-errors=n qui est sans doute un bon compromis (erreurs identiques à cause de copier/coller par exemple).
    Absolument, tout cela est très pertinent mais effectivement, on prend en général rapidement l'habitude du « zero warning », donc on les corrige toujours quand ils se produisent… à condition là encore d'avoir bien compris ce qui les provoque, au risque d'apporter une solution qui soit pire que le mal. Par ailleurs, les « cascades d'erreurs » sont généralement provoquées par la première de la liste. Donc en prenant l'habitude de commencer par elle, on parvient rapidement à les résoudre toutes également. C'est pour cela que prendre le temps de revoir toutes les options de compilation est un excellent exercice aussi.

  17. #17
    Membre confirmé
    Profil pro
    Inscrit en
    Avril 2012
    Messages
    103
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Avril 2012
    Messages : 103
    Par défaut
    Bonjour Guesset

    Merci d'intervenir. Comme son nom l'indique, ce programme n'est pas censé faire quelque chose sinon montrer un problème.
    Si on remarque, comme je l'ai fais très tardivement, l'incohérence de l'absence de return, on corrige et il n'y a plus de problème.
    En fait il y deux manières de corriger : soit on place un return j (par exemple) soit on déclare void la fonction foo().
    Dans l'exemple que tu donnes, il n'y a pas d'erreur : donc il n'y a plus rien à voir !
    Tu dis
    // b1 supprimé car toujours vrai dans la boucle
    Mais avec l'incohérence non corrigé, b1 montre que ce n'est pas vrai dans la boucle alors que cela le devrait.
    Je ne comprenais pas comment ce programme pouvait boucler à l'infini. Donc j'ai imprimé la valeur de 'x' et trouvé '0'.
    Bizarre non, dans une boucle while(x != 0). Pour enfoncer le clou, j'ai imprimé b1 = (x != 0) et même b1 = static_cast<bool>(x) et trouvé '1'.
    Re-bizarre non ? Etc.

    La discussion entre Jo_link_noir et Obsidian porte, si j'ai bien compris, sur le pourquoi du comment de ce comportement bizarre.

  18. #18
    Membre Expert

    Homme Profil pro
    Directeur de projet
    Inscrit en
    Mai 2013
    Messages
    1 820
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Directeur de projet
    Secteur : Service public

    Informations forums :
    Inscription : Mai 2013
    Messages : 1 820
    Par défaut
    Bonjour Serge,

    Citation Envoyé par serge17 Voir le message
    ...Mais avec l'incohérence non corrigée, b1 montre que ce n'est pas vrai dans la boucle alors que cela le devrait...
    Autant les analyses du comportement sont fines et intéressantes, autant elles tentent d'expliquer le comportement d'une seule chaine de compilation alors que la manifestation de l'incohérence sera certainement différente selon les compilateurs et les niveaux d'optimisations.

    Salut
    Ever tried. Ever failed. No matter. Try Again. Fail again. Fail better. (Samuel Beckett)

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. images diffrentes dans l' infobulle
    Par lloyd450 dans le forum APIs Google
    Réponses: 0
    Dernier message: 22/03/2012, 16h05
  2. Comportement différent dans JTable
    Par scorbo dans le forum Composants
    Réponses: 3
    Dernier message: 21/03/2012, 14h05
  3. Comportement étrange dans boucles foreach
    Par Tyra3l dans le forum Langage
    Réponses: 5
    Dernier message: 17/02/2012, 23h30
  4. Comportement bizarre dans POI
    Par bruno.wiesen dans le forum Documents
    Réponses: 1
    Dernier message: 23/05/2007, 11h04

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo