Pourquoi cette boucle produit-elle «warning: l’itération 3u invoque un comportement indéfini» et génère plus de 4 lignes?

Comstackr ceci:

#include  int main() { for (int i = 0; i < 4; ++i) std::cout << i*1000000000 << std::endl; } 

et gcc génère l’avertissement suivant:

 warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations] std::cout << i*1000000000 << std::endl; ^ 

Je comprends qu’il y a un dépassement d’entier signé.

Qu’est-ce que je ne peux pas obtenir, c’est pourquoi i valeur est cassé par cette opération de débordement?

J’ai lu les réponses à Pourquoi le débordement d’entier sur x86 avec GCC provoque-t-il une boucle infinie? , mais je ne comprends toujours pas pourquoi cela se produit – je comprends que “indéfini” signifie “tout peut arriver”, mais quelle est la cause sous-jacente de ce comportement spécifique ?

En ligne: http://ideone.com/dMrRKR

Compilateur: gcc (4.8)

    Débordement d’entier signé (à proprement parler, il n’existe pas de “débordement d’entier non signé”) signifie un comportement indéfini . Et cela signifie que tout peut arriver, et parler de pourquoi cela se produit selon les règles du C ++ n’a pas de sens.

    C ++ 11 draft N3337: §5.4: 1

    Si, lors de l’évaluation d’une expression, le résultat n’est pas défini mathématiquement ou ne figure pas dans la plage des valeurs représentables pour son type, le comportement est indéfini. [Note: la plupart des implémentations existantes de C ++ ignorent les débordements d’entiers. Le traitement de la division par zéro, formant un rest à l’aide d’un diviseur zéro, et toutes les exceptions à virgule flottante varient selon les machines, et sont généralement ajustables par une fonction de bibliothèque. —End note]

    Votre code compilé avec g++ -O3 émet un avertissement (même sans -Wall )

     a.cpp: In function 'int main()': a.cpp:11:18: warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations] std::cout << i*1000000000 << std::endl; ^ a.cpp:9:2: note: containing loop for (int i = 0; i < 4; ++i) ^ 

    La seule façon d'parsingr ce que fait le programme est de lire le code assembleur généré.

    Voici la liste complète des assemblages:

      .file "a.cpp" .section .text$_ZNKSt5ctypeIcE8do_widenEc,"x" .linkonce discard .align 2 LCOLDB0: LHOTB0: .align 2 .p2align 4,,15 .globl __ZNKSt5ctypeIcE8do_widenEc .def __ZNKSt5ctypeIcE8do_widenEc; .scl 2; .type 32; .endef __ZNKSt5ctypeIcE8do_widenEc: LFB860: .cfi_startproc movzbl 4(%esp), %eax ret $4 .cfi_endproc LFE860: LCOLDE0: LHOTE0: .section .text.unlikely,"x" LCOLDB1: .text LHOTB1: .p2align 4,,15 .def ___tcf_0; .scl 3; .type 32; .endef ___tcf_0: LFB1091: .cfi_startproc movl $__ZStL8__ioinit, %ecx jmp __ZNSt8ios_base4InitD1Ev .cfi_endproc LFE1091: .section .text.unlikely,"x" LCOLDE1: .text LHOTE1: .def ___main; .scl 2; .type 32; .endef .section .text.unlikely,"x" LCOLDB2: .section .text.startup,"x" LHOTB2: .p2align 4,,15 .globl _main .def _main; .scl 2; .type 32; .endef _main: LFB1084: .cfi_startproc leal 4(%esp), %ecx .cfi_def_cfa 1, 0 andl $-16, %esp pushl -4(%ecx) pushl %ebp .cfi_escape 0x10,0x5,0x2,0x75,0 movl %esp, %ebp pushl %edi pushl %esi pushl %ebx pushl %ecx .cfi_escape 0xf,0x3,0x75,0x70,0x6 .cfi_escape 0x10,0x7,0x2,0x75,0x7c .cfi_escape 0x10,0x6,0x2,0x75,0x78 .cfi_escape 0x10,0x3,0x2,0x75,0x74 xorl %edi, %edi subl $24, %esp call ___main L4: movl %edi, (%esp) movl $__ZSt4cout, %ecx call __ZNSolsEi movl %eax, %esi movl (%eax), %eax subl $4, %esp movl -12(%eax), %eax movl 124(%esi,%eax), %ebx testl %ebx, %ebx je L15 cmpb $0, 28(%ebx) je L5 movsbl 39(%ebx), %eax L6: movl %esi, %ecx movl %eax, (%esp) addl $1000000000, %edi call __ZNSo3putEc subl $4, %esp movl %eax, %ecx call __ZNSo5flushEv jmp L4 .p2align 4,,10 L5: movl %ebx, %ecx call __ZNKSt5ctypeIcE13_M_widen_initEv movl (%ebx), %eax movl 24(%eax), %edx movl $10, %eax cmpl $__ZNKSt5ctypeIcE8do_widenEc, %edx je L6 movl $10, (%esp) movl %ebx, %ecx call *%edx movsbl %al, %eax pushl %edx jmp L6 L15: call __ZSt16__throw_bad_castv .cfi_endproc LFE1084: .section .text.unlikely,"x" LCOLDE2: .section .text.startup,"x" LHOTE2: .section .text.unlikely,"x" LCOLDB3: .section .text.startup,"x" LHOTB3: .p2align 4,,15 .def __GLOBAL__sub_I_main; .scl 3; .type 32; .endef __GLOBAL__sub_I_main: LFB1092: .cfi_startproc subl $28, %esp .cfi_def_cfa_offset 32 movl $__ZStL8__ioinit, %ecx call __ZNSt8ios_base4InitC1Ev movl $___tcf_0, (%esp) call _atexit addl $28, %esp .cfi_def_cfa_offset 4 ret .cfi_endproc LFE1092: .section .text.unlikely,"x" LCOLDE3: .section .text.startup,"x" LHOTE3: .section .ctors,"w" .align 4 .long __GLOBAL__sub_I_main .lcomm __ZStL8__ioinit,1,1 .ident "GCC: (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.0" .def __ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef .def __ZNSolsEi; .scl 2; .type 32; .endef .def __ZNSo3putEc; .scl 2; .type 32; .endef .def __ZNSo5flushEv; .scl 2; .type 32; .endef .def __ZNKSt5ctypeIcE13_M_widen_initEv; .scl 2; .type 32; .endef .def __ZSt16__throw_bad_castv; .scl 2; .type 32; .endef .def __ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef .def _atexit; .scl 2; .type 32; .endef 

    Je peux à peine lire l'assemblage, mais je peux même voir la ligne addl $1000000000, %edi . Le code résultant ressemble plus à

     for(int i = 0; /* nothing, that is - infinite loop */; i += 1000000000) std::cout << i << std::endl; 

    Ce commentaire de @TC:

    Je pense que c'est quelque chose comme: (1) parce que chaque itération avec i de valeur supérieure à 2 a un comportement indéfini -> (2) on peut supposer que i <= 2 à des fins d'optimisation -> (3) true -> (4) il est optimisé dans une boucle infinie.

    m'a donné l'idée de comparer le code d'assemblage du code de l'OP au code d'assemblage du code suivant, sans comportement indéfini.

     #include  int main() { // changed the termination condition for (int i = 0; i < 3; ++i) std::cout << i*1000000000 << std::endl; } 

    Et, en fait, le code correct a une condition de terminaison.

      ; ...snip... L6: mov ecx, edi mov DWORD PTR [esp], eax add esi, 1000000000 call __ZNSo3putEc sub esp, 4 mov ecx, eax call __ZNSo5flushEv cmp esi, -1294967296 // here it is jne L7 lea esp, [ebp-16] xor eax, eax pop ecx ; ...snip... 

    OMG, c'est complètement pas évident! Ce n'est pas juste! Je demande un procès par le feu!

    Traitez-le, vous avez écrit le code buggy et vous devriez vous sentir mal. Supporter les conséquences

    ... ou bien utiliser de meilleurs diagnostics et de meilleurs outils de débogage - voilà ce à quoi ils servent:

    • activer tous les avertissements

      • -Wall est l'option gcc qui active tous les avertissements utiles sans faux positifs. C'est un ssortingct minimum que vous devez toujours utiliser.
      • gcc a beaucoup d'autres options d'avertissement , cependant, ils ne sont pas activés avec -Wall car ils peuvent avertir des faux positifs
      • Visual C ++ est malheureusement à la traîne avec la possibilité de donner des avertissements utiles. Au moins, l'IDE en permet par défaut.
    • utiliser des indicateurs de débogage pour le débogage

      • pour débordement d'entier -ftrapv le programme en cas de débordement,
      • Le compilateur Clang est excellent pour cela: -fcatch-undefined-behavior attrape beaucoup d'instances de comportement indéfini (note: "a lot of" != "all of them" )

    J'ai un désordre de spaghetti d'un programme que je n'ai pas écrit et qui doit être expédié demain! AIDE !!!!!! 111oneone

    Utilisez -fwrapv de gcc

    Cette option indique au compilateur de supposer que le dépassement arithmétique signé d’addition, de soustraction et de multiplication se répercute en utilisant la représentation à deux complément.

    1 - cette règle ne s'applique pas au "débordement d'entier non signé", comme le dit le §3.9.1.4

    Les entiers non signés, déclarés non signés, doivent obéir aux lois de l'arithmétique modulo 2 n où n est le nombre de bits de la représentation des valeurs de la taille entière entière.

    et par exemple le résultat de UINT_MAX + 1 est défini mathématiquement - par les règles de l'arithmétique modulo 2 n

    Réponse courte, gcc spécifiquement documenté ce problème, nous pouvons le voir dans les notes de publication de gcc 4.8, qui dit:

    GCC utilise maintenant une parsing plus agressive pour dériver une limite supérieure du nombre d’itérations de boucles en utilisant les contraintes imposées par les normes de langage . Cela peut empêcher les programmes non conformes de fonctionner comme prévu, tels que les processeurs SPEC 2006 464.h264ref et 416.gamess. Une nouvelle option, optimisations de boucle -fno-agressif, a été ajoutée pour désactiver cette parsing agressive. Dans certaines boucles qui ont connu un nombre constant d’itérations, mais dont le comportement indéfini se produit dans la boucle avant ou pendant la dernière itération, GCC avertira du comportement indéfini dans la boucle au lieu de dériver la limite supérieure inférieure du nombre d’itérations pour la boucle. L’avertissement peut être désactivé avec les optimisations de boucle -Wno-agressives.

    et en effet, si nous utilisons des -fno-aggressive-loop-optimizations le comportement de la boucle infinie devrait cesser et cela se produit dans tous les cas que j’ai testés.

    La réponse longue commence par savoir que le dépassement d’ entier signé est un comportement indéfini en examinant le paragraphe 4 de la section 5 expressions standard C ++:

    Si, lors de l’évaluation d’une expression, le résultat n’est pas défini mathématiquement ou ne figure pas dans la plage des valeurs représentables pour son type, le comportement n’est pas défini . [Note: la plupart des implémentations existantes de C ++ ignorent les débordements d’entiers. Le traitement de la division par zéro, formant un rest à l’aide d’un diviseur zéro, et toutes les exceptions à virgule flottante varient d’une machine à l’autre et sont généralement ajustables par une fonction de bibliothèque. —End note

    Nous soaps que la norme dit que le comportement indéfini est imprévisible à partir de la note accompagnant la définition:

    [Note: Un comportement indéfini peut être attendu lorsque la présente Norme internationale omet toute définition explicite du comportement ou lorsqu’un programme utilise une construction erronée ou des données erronées. Le comportement indéfini admissible peut aller de l’ignorance complète de la situation à des résultats imprévisibles , au comportement lors de la traduction ou à l’exécution du programme d’une manière documentée caractéristique de l’environnement (avec ou sans message de diagnostic), à la fin de la traduction ou de l’exécution d’un message de diagnostic). De nombreuses constructions de programme erronées n’engendrent pas un comportement indéfini; ils doivent être diagnostiqués. —End note]

    Mais que peut faire l’optimiseur gcc au monde pour en faire une boucle infinie? Cela semble complètement farfelu. Mais heureusement, gcc nous donne un indice pour comprendre cet avertissement:

     warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations] std::cout << i*1000000000 << std::endl; ^ 

    La clé est l’ Waggressive-loop-optimizations , ça veut dire quoi? Heureusement pour nous, ce n’est pas la première fois que cette optimisation a cassé le code de cette façon et nous avons de la chance car John Regehr a documenté un cas dans l’article GCC avant 4.8 Breaks Broken SPEC 2006 Benchmarks .

     int d[16]; int SATD (void) { int satd = 0, dd, k; for (dd=d[k=0]; k<16; dd=d[++k]) { satd += (dd < 0 ? -dd : dd); } return satd; } 

    l'article dit:

    Le comportement indéfini accède à d [16] juste avant de quitter la boucle. En C99, il est légal de créer un pointeur sur un élément situé après la fin du tableau, mais ce pointeur ne doit pas être déréférencé.

    et plus tard dit:

    En détail, voici ce qui se passe. Le compilateur AC, en voyant d [++ k], est autorisé à supposer que la valeur incrémentée de k se situe dans les limites du tableau, car un comportement non défini se produit sinon. Pour le code ici, GCC peut déduire que k est dans la plage 0..15. Un peu plus tard, lorsque GCC voit k <16, il se dit: «Aha– cette expression est toujours vraie, donc nous avons une boucle infinie.» La situation ici, où le compilateur utilise l’hypothèse de bonne définition pour inférer une fait utile de stream de données,

    Donc, dans certains cas, le compilateur doit supposer que le dépassement d'entier signé est un comportement indéfini, alors i dois toujours être inférieur à 4 et nous avons donc une boucle infinie.

    Il explique que ceci est très similaire à la suppression de la vérification du pointeur null du kernel Linux dans ce code:

     struct foo *s = ...; int x = s->f; if (!s) return ERROR; 

    gcc déduit que s était déférencé dans s->f; et puisque le déréférencement d'un pointeur nul est un comportement indéfini, alors s ne doit pas être nul et optimise donc le contrôle if (!s) sur la ligne suivante.

    La leçon ici est que les optimiseurs modernes sont très agressifs dans l’exploitation de comportements indéfinis et qu’ils ne seront probablement plus agressifs. Avec quelques exemples, nous pouvons clairement voir que l'optimiseur fait des choses qui semblent complètement déraisonnables pour un programmeur, mais rétrospectivement du sharepoint vue des optimiseurs.

    tl; dr Le code génère un test de type entier + entier positif == entier négatif . Généralement, l’optimiseur ne l’optimise pas, mais dans le cas spécifique où std::endl est utilisé ensuite, le compilateur optimise ce test. Je n’ai pas encore trouvé ce qui est spécial avec endl .


    A partir du code d’assemblage à -O1 et aux niveaux supérieurs, il est clair que gcc réorganise la boucle pour:

     i = 0; do { cout << i << endl; i += NUMBER; } while (i != NUMBER * 4) 

    La plus grande valeur qui fonctionne correctement est 715827882 , c'est-à-dire étage ( INT_MAX/3 ). L'extrait d'assembly à -O1 est:

     L4: movsbl %al, %eax movl %eax, 4(%esp) movl $__ZSt4cout, (%esp) call __ZNSo3putEc movl %eax, (%esp) call __ZNSo5flushEv addl $715827882, %esi cmpl $-1431655768, %esi jne L6 // fallthrough to "return" code 

    Notez que le -1431655768 est 4 * 715827882 en complément à 2.

    Frapper -O2 optimise ce qui suit:

     L4: movsbl %al, %eax addl $715827882, %esi movl %eax, 4(%esp) movl $__ZSt4cout, (%esp) call __ZNSo3putEc movl %eax, (%esp) call __ZNSo5flushEv cmpl $-1431655768, %esi jne L6 leal -8(%ebp), %esp jne L6 // fallthrough to "return" code 

    Donc, l'optimisation qui a été faite est simplement que l' addl été déplacé plus haut.

    Si nous recompilons avec 715827883 au lieu de cela, la version -O1 est identique en dehors du numéro et de la valeur de test modifiés. Cependant, -O2 fait alors un changement:

     L4: movsbl %al, %eax addl $715827883, %esi movl %eax, 4(%esp) movl $__ZSt4cout, (%esp) call __ZNSo3putEc movl %eax, (%esp) call __ZNSo5flushEv jmp L2 

    Où il y avait cmpl $-1431655764, %esi à -O1 , cette ligne a été supprimée pour -O2 . L'optimiseur doit avoir décidé que l'ajout de 715827883 à %esi ne peut jamais être égal à -1431655764 .

    C'est plutôt déroutant. Ajouter cela à INT_MIN+1 génère le résultat attendu, donc l'optimiseur doit avoir décidé que %esi ne peut jamais être INT_MIN+1 et je ne suis pas sûr de savoir pourquoi.

    Dans l'exemple de travail, il semble tout aussi valable de conclure que l'ajout de 715827882 à un nombre ne peut pas être égal à INT_MIN + 715827882 - 2 ! (ceci n'est possible que si le wraparound se produit réellement), mais n'optimise pas la ligne dans cet exemple.


    Le code que j'utilisais est:

     #include  #include  int main() { for (int i = 0; i < 4; ++i) { //volatile int j = i*715827883; volatile int j = i*715827882; printf("%d\n", j); std::endl(std::cout); } } 

    Si std::endl(std::cout) est supprimé, l'optimisation ne se produit plus. En fait, le remplacer par std::cout.put('\n'); std::flush(std::cout); std::cout.put('\n'); std::flush(std::cout); provoque également l'optimisation ne se produit pas, même si std::endl est en std::endl .

    L'inlining de std::endl semble affecter la partie antérieure de la structure de la boucle (que je ne comprends pas très bien ce qu'elle fait mais je la posterai ici si quelqu'un d'autre le fait):

    Avec code original et -O2 :

     L2: movl %esi, 28(%esp) movl 28(%esp), %eax movl $LC0, (%esp) movl %eax, 4(%esp) call _printf movl __ZSt4cout, %eax movl -12(%eax), %eax movl __ZSt4cout+124(%eax), %ebx testl %ebx, %ebx je L10 cmpb $0, 28(%ebx) je L3 movzbl 39(%ebx), %eax L4: movsbl %al, %eax addl $715827883, %esi movl %eax, 4(%esp) movl $__ZSt4cout, (%esp) call __ZNSo3putEc movl %eax, (%esp) call __ZNSo5flushEv jmp L2 // no test 

    Avec mon insertion manuelle de std::endl , -O2 :

     L3: movl %ebx, 28(%esp) movl 28(%esp), %eax addl $715827883, %ebx movl $LC0, (%esp) movl %eax, 4(%esp) call _printf movl $10, 4(%esp) movl $__ZSt4cout, (%esp) call __ZNSo3putEc movl $__ZSt4cout, (%esp) call __ZNSo5flushEv cmpl $-1431655764, %ebx jne L3 xorl %eax, %eax 

    Une différence entre ces deux est que %esi est utilisé dans l'original et %ebx dans la deuxième version; Y a-t-il une différence de sémantique entre %esi et %ebx en général? (Je ne connais pas beaucoup l'assemblage x86).

    Qu’est-ce que je ne peux pas obtenir est pourquoi je valeur est cassé par cette opération de débordement?

    Il semble que le débordement d’entier se produit dans la 4ème itération (pour i = 3 ). signed dépassement d’entier signed appelle un comportement non défini . Dans ce cas, rien ne peut être prédit. La boucle ne peut itérer que 4 fois ou aller à l’infini ou à toute autre chose!
    Le résultat peut varier du compilateur au compilateur ou même pour différentes versions du même compilateur.

    C11: 1.3.24 comportement indéfini:

    comportement pour lequel la présente Norme internationale n’impose aucune exigence
    [Note: Un comportement indéfini peut être attendu lorsque la présente Norme internationale omet toute définition explicite du comportement ou lorsqu’un programme utilise une construction erronée ou des données erronées. Le comportement indéfini admissible peut aller de l’ignorance complète de la situation à des résultats imprévisibles, au comportement lors de la traduction ou à l’exécution du programme d’une manière documentée caractéristique de l’environnement (avec ou sans message de diagnostic), à la fin de la traduction ou de l’exécution d’un message de diagnostic) . De nombreuses constructions de programme erronées n’engendrent pas un comportement indéfini; ils doivent être diagnostiqués. —End note]

    Un autre exemple de cette erreur signalée dans gcc est lorsque vous avez une boucle qui s’exécute pour un nombre constant d’itérations, mais que vous utilisez la variable counter en tant qu’index dans un tableau contenant moins d’éléments, tels que:

     int a[50], x; for( i=0; i < 1000; i++) x = a[i]; 

    Le compilateur peut déterminer que cette boucle essaiera d'accéder à la mémoire en dehors du tableau 'a'. Le compilateur se plaint à ce sujet avec ce message plutôt cryptique:

    itération xxu invoque un comportement indéfini [-Werror = optimisation des boucles agressives]