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ébordement d'entier signé.
Ce que je ne peux pas obtenir, c'est pourquoi la i
valeur 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 sais toujours pas pourquoi cela se produit - j'obtiens 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)
c++
gcc
undefined-behavior
zerkms
la source
la source
O2
, etO3
, mais pasO0
ouO1
Réponses:
Un débordement d'entier signé (à proprement parler, il n'y a pas de "débordement d'entier non signé") signifie un comportement indéfini . Et cela signifie que tout peut arriver, et discuter de pourquoi cela se produit selon les règles de C ++ n'a pas de sens.
Projet C ++ 11 N3337: §5.4: 1
Votre code compilé avec
g++ -O3
émet un avertissement (même sans-Wall
)La seule façon d'analyser ce que fait le programme est de lire le code d'assemblage généré.
Voici la liste complète de l'assemblage:
Je peux à peine lire l'assemblage, mais même je peux voir la
addl $1000000000, %edi
ligne. Le code résultant ressemble plus àCe commentaire de @TC:
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.
Et, en fait, le code correct a une condition de terminaison.
Traitez-le, vous avez écrit le code du buggy et vous devriez vous sentir mal. Supportez les conséquences.
... ou, alternativement, utilisez correctement de meilleurs diagnostics et de meilleurs outils de débogage - c'est à cela qu'ils servent:
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 devez toujours utiliser.-Wall
car elles peuvent avertir des faux positifsutiliser des indicateurs de débogage pour le débogage
-ftrapv
intercepte le programme en cas de débordement,-fcatch-undefined-behavior
attrape beaucoup de cas de comportement non défini (note:"a lot of" != "all of them"
)Utilisez gcc
-fwrapv
1 - cette règle ne s'applique pas au "débordement d'entier non signé", comme le §3.9.1.4 dit que
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 nla source
i
-il affecté? En général, un comportement non défini est de ne pas avoir ce genre d'effets secondaires étranges, après tout,i*100000000
devrait être une valeuri
une valeur supérieure à 2 a un comportement non défini -> (2) nous pouvons supposer qu'ài <= 2
des fins d'optimisation -> (3) la condition de boucle est toujours vraie -> (4 ) il est optimisé dans une boucle infinie.i
de 1e9 à chaque itération (et en modifiant la condition de la boucle en conséquence). Il s'agit d'une optimisation parfaitement valable selon la règle du «comme si» car ce programme ne pourrait pas observer la différence s'il se comportait bien. Hélas, ce n'est pas le cas, et l'optimisation «fuit».Réponse courte, a
gcc
spécifiquement documenté ce problème, nous pouvons voir que dans les notes de publication de gcc 4.8 qui dit ( je souligne à l'avenir ):et en effet si nous utilisons
-fno-aggressive-loop-optimizations
la boucle infinie, le comportement devrait cesser et c'est le cas dans tous les cas que j'ai testés.La réponse longue commence par savoir que le débordement d' entier signé est un comportement indéfini en regardant le projet de section standard C ++
5
Expressions paragraphe 4 qui dit:Nous savons que la norme dit qu'un comportement non défini est imprévisible d'après la note fournie avec la définition qui dit:
Mais que diable peut faire l'
gcc
optimiseur pour transformer cela en une boucle infinie? Cela semble complètement farfelu. Mais heureusement,gcc
nous donne un indice pour le comprendre dans l'avertissement: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 pre-4.8 Breaks Broken SPEC 2006 Benchmarks qui montre le code suivant:l'article dit:
et plus tard dit:
Donc, ce que le compilateur doit faire dans certains cas, c'est supposer que le débordement d'entier signé est un comportement indéfini, alors
i
doit toujours être inférieur à4
et donc nous avons une boucle infinie.Il explique que cela est très similaire à la tristement célèbre suppression de la vérification du pointeur nul du noyau Linux où en voyant ce code:
gcc
déduit que puisque as
été déférencé danss->f;
et puisque le déréférencement d'un pointeur nul est un comportement indéfini, alorss
ne doit pas être nul et donc optimise laif (!s)
vérification sur la ligne suivante.La leçon ici est que les optimiseurs modernes sont très agressifs pour exploiter un comportement non défini et ne seront probablement que plus agressifs. Clairement, avec juste 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 des optimiseurs, cela a du sens.
la source
tl; dr Le code génère un test que entier + entier positif == entier négatif . Habituellement, l'optimiseur n'optimise pas cela, mais dans le cas spécifique d'
std::endl
être utilisé ensuite, le compilateur optimise ce test. Je n'ai pas encore compris ce qui est spécialendl
.À partir du code d'assemblage à -O1 et aux niveaux supérieurs, il est clair que gcc refactorise la boucle en:
La plus grande valeur qui fonctionne correctement est
715827882
, ie floor (INT_MAX/3
). L'extrait d'assembly à-O1
est:Notez que le
-1431655768
est4 * 715827882
en complément de 2.Frapper
-O2
optimise cela comme suit:Ainsi, l'optimisation qui a été faite est simplement que le a
addl
été déplacé plus haut.Si nous recompilons avec à la
715827883
place, la version -O1 est identique à l'exception du nombre et de la valeur de test modifiés. Cependant, -O2 fait alors un changement:Là où il y avait
cmpl $-1431655764, %esi
à-O1
, cette ligne a été supprimé car-O2
. L'optimiseur doit avoir décidé que l'ajout715827883
à%esi
ne peut jamais égaler-1431655764
.C'est assez déroutant. Ajoutant que pour
INT_MIN+1
ne générer le résultat attendu, de sorte que l'optimiseur doit avoir décidé que%esi
ne peut jamais êtreINT_MIN+1
et je ne sais pas pourquoi il déciderait que.Dans l'exemple de travail, il semble qu'il serait également valable de conclure que l'ajout
715827882
à un nombre ne peut pas égalerINT_MIN + 715827882 - 2
! (cela n'est possible que si le bouclage se produit réellement), mais cela n'optimise pas la sortie de ligne dans cet exemple.Le code que j'utilisais est:
Si le
std::endl(std::cout)
est supprimé, l'optimisation ne se produit plus. En fait, le remplacer parstd::cout.put('\n'); std::flush(std::cout);
empêche également l'optimisation, même si ellestd::endl
est intégrée.L'inlining de
std::endl
semble affecter la partie antérieure de la structure de la boucle (ce que je ne comprends pas très bien ce qu'elle fait mais je la posterai ici au cas où quelqu'un d'autre le ferait):Avec code d'origine et
-O2
:Avec inline mymanual de
std::endl
,-O2
:Une différence entre ces deux est qu'il
%esi
est utilisé dans l'original et%ebx
dans la deuxième version; y a-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 l'assemblage x86).la source
Un autre exemple de cette erreur signalée dans gcc est lorsque vous avez une boucle qui s'exécute pendant un nombre constant d'itérations, mais que vous utilisez la variable de compteur comme index dans un tableau qui a moins que ce nombre d'éléments, comme:
Le compilateur peut déterminer que cette boucle tentera d'accéder à la mémoire en dehors du tableau «a». Le compilateur s'en plaint avec ce message plutôt cryptique:
la source
Il semble qu'un débordement d'entier se produise dans la 4e itération (pour
i = 3
).signed
le dépassement d'entier appelle un comportement indéfini . Dans ce cas, rien ne peut être prédit. La boucle peut itérer seulement des4
fois ou aller à l'infini ou 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 non défini:
la source