web-dev-qa-db-fra.com

Pourquoi cette boucle génère-t-elle "warning: l'itération 3u appelle un comportement indéfini" et génère plus de 4 lignes?

Compiler ceci:

#include <iostream>

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

et gcc produit 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é.

Ce que je ne peux pas comprendre, c'est pourquoi la valeur i est cassée par cette opération de débordement?

J'ai lu les réponses à Pourquoi un débordement d'entier sur x86 avec GCC provoque-t-il une boucle infinie? , mais je ne comprends toujours pas bien pourquoi cela arrive - je comprends que "non défini" signifie que "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)

156
zerkms

Débordement d'entier signé (au sens strict, "débordement d'entier non signé" n'existe pas) signifie comportement indéfini . Et cela signifie que tout peut arriver, et discuter pourquoi cela se produit sous 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 n'est pas compris dans la plage de valeurs représentables pour son type, le comportement est indéfini. [Remarque: la plupart des implémentations existantes de C++ ignorent les débordements d'entiers. Le traitement de la division par zéro, formant un reste utilisant un diviseur de zéro, et toutes les exceptions en virgule flottante, varie d’une machine à l’autre et peut généralement être ajusté par une fonction de bibliothèque. —Fin 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)
  ^

Le seul moyen d’analyser ce que fait le programme est de lire le code Assembly généré.

Voici la liste complète de l'Assemblée:

    .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 Assembly, mais même moi, je peux 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 @ T.C .:

Je suppose que cela ressemble à quelque chose comme: (1) parce que chaque itération avec i de toute valeur supérieure à 2 a un comportement indéfini -> (2) nous pouvons supposer que i <= 2 à des fins d'optimisation -> (3) la condition de boucle est toujours vraie -> (4) elle est optimisée pour former 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 <iostream>

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! J'exige un procès au feu!

Traite-toi, tu as écrit le code du buggy et tu devrais te sentir mal. Portez les conséquences.

... ou bien utiliser de meilleurs diagnostics et de meilleurs outils de débogage - c'est ce qu'ils sont:

  • activer tous les avertissements

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

    • pour un dépassement d'entier -ftrapv interrompt le programme en cas de dépassement de capacité,
    • Le compilateur Clang est excellent pour cela: -fcatch-undefined-behavior intercepte un grand nombre d'instances de comportement indéfini (note: "a lot of" != "all of them")

J'ai un programme de spaghettis non écrit par moi qui doit être expédié demain! AIDE !!!!!! 111oneone

Utiliser le -fwrapv de gcc

Cette option indique au compilateur de supposer que le dépassement arithmétique signé des enveloppes d’addition, de soustraction et de multiplication utilise une représentation à deux compléments.

1 - cette règle ne s'applique pas au "dépassement d'entier non signé", car le § 3..9.1.4 dit que

Les entiers non signés, déclarés non signés, doivent obéir aux lois de l'arithmétique modulo 2n où n est le nombre de bits dans la représentation de la valeur de cette taille particulière d’entier.

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

104
milleniumbug

Réponse courte, gcc a spécifiquement documenté ce problème, nous pouvons le voir dans = notes de mise à jour de gcc 4.8 qui dit (accentuation future):

GCC utilise maintenant une analyse plus agressive pour déduire une limite supérieure du nombre d'itérations de boucles à l'aide de contraintes imposées par les normes de langage . Par conséquent, des programmes non conformes risquent de ne plus fonctionner comme prévu, tels que SPEC CPU 2006 464.h264ref et 416.gamess. Une nouvelle option, -fno-agressive-optimisations-boucle, a été ajoutée pour désactiver cette analyse agressive. Dans certaines boucles qui ont connu un nombre constant d'itérations, mais dont on sait qu'un comportement indéfini se produit dans la boucle avant d'atteindre ou lors de la dernière itération, GCC avertit du comportement non défini dans la boucle au lieu de déduire la limite supérieure inférieure du nombre d'itérations. pour la boucle. L'avertissement peut être désactivé avec les optimisations de -Wno-agressif-boucle.

et en effet, si nous utilisons -fno-aggressive-loop-optimizations, le comportement de la boucle infinie doit cesser et il le fait dans tous les cas que j'ai testés.

La réponse longue commence par le fait de savoir que entier signé le dépassement de capacité est un comportement indéfini en examinant le projet de section standard C++ 5Expressions paragraphe 4 qui dit:

Si, lors de l'évaluation d'une expression, le résultat n'est pas défini mathématiquement ou ne fait pas partie des valeurs pouvant être représentées pour son type, le comportement est indéfini . [Remarque: la plupart des implémentations existantes de C++ ignorent les débordements d'entiers. Le traitement de la division par zéro, formant un reste utilisant un diviseur égal à zéro, et toutes les exceptions en virgule flottante varie selon les machines et est généralement ajustable par une fonction de bibliothèque. —Fin note

Nous savons que la norme stipule qu'un comportement indéfini est imprévisible à partir de la note accompagnant la définition qui dit:

[Remarque: Un comportement indéfini peut être attendu lorsque la présente Norme internationale omet toute définition explicite de comportement ou lorsqu'un programme utilise une construction ou des données erronées. Le comportement non défini autorisé va d'ignorer complètement la situation avec des résultats imprévisibles , de se comporter pendant la traduction ou l'exécution du programme de manière documentée, caractéristique de l'environnement (avec ou sans l'émission d'un message de diagnostic), à mettre fin à une traduction ou à une exécution (avec l'émission d'un message de diagnostic). Beaucoup de constructions de programme erronées n'engendrent pas un comportement indéfini; ils doivent être diagnostiqués. —Fin note]

Mais qu'est-ce que l'optimiseur gcc peut faire dans le monde pour le transformer en une boucle infinie? Cela semble complètement farfelu. Mais heureusement, gcc nous donne un indice pour le savoir dans l'avertissement:

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

L'indice est le Waggressive-loop-optimizations, qu'est-ce que cela signifie? Heureusement pour nous, ce n'est pas la première fois que cette optimisation a cassé le code de cette manière 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 qui montre le code suivant:

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 consiste à accéder à d [16] juste avant de quitter la boucle. En C99, il est légal de créer un pointeur sur un élément situé une position 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. Un compilateur C, après avoir vu d [++ k], est autorisé à supposer que la valeur incrémentée de k est comprise dans les limites du tableau, sinon un comportement indéfini se produirait. Pour le code ici, GCC peut en déduire que k est compris dans la plage 0..15. Un peu plus tard, quand GCC voit k <16, il se dit: "Aha - cette expression est toujours vraie, nous avons donc une boucle infinie." La situation ici, où le compilateur utilise l'hypothèse d'une définition précise permettant de déduire un fait utile en matière de flux de données,

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

Il explique que cela ressemble beaucoup à l'infâme suppression du contrôle du pointeur null dans le noyau Linux où voir ce code:

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

gcc déduit que, puisque s a été différé dans s->f; et que 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 if (!s) vérifiez à la ligne suivante.

La leçon à tirer est que les optimiseurs modernes exploitent de manière très agressive un comportement non défini et qu’ils ne feront probablement qu’aggraver. Clairement, avec seulement quelques exemples, nous pouvons voir que l'optimiseur fait des choses qui semblent complètement déraisonnables pour un programmeur, mais rétrospectivement, du point de vue de l'optimiseur, cela a du sens.

65
Shafik Yaghmour

tl; dr Le code génère un test qui entier + entier positif == entier négatif . Généralement, l'optimiseur n'optimise pas cette sortie, mais dans le cas particulier de std::endl utilisé par la suite, le compilateur optimise ce test. Je n'ai pas encore compris ce qui est spécial à propos de endl encore.


A partir du code d'assemblage aux niveaux -O1 et supérieurs, il est clair que gcc refactorise 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 floor (INT_MAX/3). L'extrait d'assemblage sur -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 -1431655768 est 4 * 715827882 dans le complément à 2.

Frapper -O2 optimise cela comme 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 la addl a été déplacée plus haut.

Si nous recompilons avec 715827883 à la place, la version -O1 est identique, à l'exception du nombre modifié et de la valeur de test. Cependant, -O2 apporte 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 assez déroutant. Ajouter cela à INT_MIN+1 génère le résultat attendu. L'optimiseur doit donc avoir décidé que %esi ne peut jamais être INT_MIN+1 et je ne sais pas pourquoi cela serait décidé.

Dans l'exemple de travail, il semble tout aussi correct de conclure que l'ajout de 715827882 à un nombre ne peut être égal à INT_MIN + 715827882 - 2! (ceci n'est possible que si le bouclage a effectivement lieu), mais n'optimise pas la sortie de ligne dans cet exemple.


Le code que j'utilisais est:

#include <iostream>
#include <cstdio>

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 la std::endl(std::cout) est supprimée, l'optimisation ne se produit plus. En fait, le remplacer par std::cout.put('\n'); std::flush(std::cout); empêche également l'optimisation, même si std::endl est en ligne.

L'inline de std::endl semble affecter la partie précédente de la structure de la boucle (ce 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 le code d'origine 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 l'inclusion 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 méthodes est que %esi est utilisé dans l'original et %ebx dans la deuxième version; Existe-t-il une différence de sémantique définie entre %esi et %ebx en général? (Je ne sais pas grand chose sur x86 Assembly).

23
M.M

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

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 s'en plaint avec ce message plutôt crypté:

itération xxu invoque un comportement non défini [-Werror = agress-loop-optimizations]

6
Ed Tyler

Ce que je ne peux pas comprendre, c'est pourquoi la valeur i est cassée par cette opération de débordement?

Il semble que le dépassement d'entier se produise à la 4ème itération (pour i = 3). signed dépassement d'entier invoque comportement non défini. Dans ce cas, rien ne peut être prédit. La boucle peut n'itérer que 4 fois ou elle peut aller à l'infini ou autre chose!
Le résultat peut varier d'un compilateur à l'autre ou même d'une version à l'autre du même compilateur.

C11: 1.3.24 comportement non défini:

comportement pour lequel la présente Norme internationale n'impose aucune exigence
[Remarque: Un comportement non défini peut être attendu lorsque la présente Norme internationale omet toute définition explicite du comportement ou lorsqu'un programme utilise une construction ou des données erronées. Le comportement non défini autorisé va d'ignorer complètement la situation avec des résultats imprévisibles, de se comporter pendant la traduction ou l'exécution du programme de manière documentée, caractéristique de l'environnement (avec ou sans émission d'un message de diagnostic), jusqu'à la fin de la traduction ou de l'exécution (avec l'émission d'un message de diagnostic). Beaucoup de constructions de programme erronées n'engendrent pas un comportement indéfini; ils doivent être diagnostiqués. —Fin note]

6
haccks