Zusammenstellung:
#include <iostream>
int main()
{
for (int i = 0; i < 4; ++i)
std::cout << i*1000000000 << std::endl;
}
und gcc
erzeugt die folgende Warnung:
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
Ich verstehe, dass es einen vorzeichenbehafteten Ganzzahlüberlauf gibt.
Was ich nicht bekommen kann ist, warum der i
Wert durch diese Überlaufoperation gebrochen wird?
Ich habe die Antworten auf Warum verursacht ein Ganzzahlüberlauf auf x86 mit GCC eine Endlosschleife? , aber ich bin mir immer noch nicht sicher, warum dies passiert - ich verstehe , dass "undefiniert" bedeutet, dass "alles passieren kann", aber was ist die zugrunde liegende Ursache für dieses spezifische Verhalten ?
Online: http://ideone.com/dMrRKR
Compiler: gcc (4.8)
c++
gcc
undefined-behavior
zerkms
quelle
quelle
O2
, undO3
Flagge, aber nichtO0
oderO1
Antworten:
Vorzeichenbehafteter Ganzzahlüberlauf (genau genommen gibt es keinen "vorzeichenlosen Ganzzahlüberlauf") bedeutet undefiniertes Verhalten . Dies bedeutet, dass alles passieren kann und es keinen Sinn macht zu diskutieren, warum dies nach den Regeln von C ++ geschieht.
C ++ 11 Entwurf N3337: §5.4: 1
Ihr mit kompilierter Code
g++ -O3
gibt Warnung aus (auch ohne-Wall
)Die einzige Möglichkeit, zu analysieren, was das Programm tut, besteht darin, den generierten Assemblycode zu lesen.
Hier ist die vollständige Baugruppenliste:
Ich kann kaum Baugruppen lesen, aber selbst ich kann die
addl $1000000000, %edi
Linie sehen. Der resultierende Code sieht eher so ausDieser Kommentar von @TC:
gab mir die Idee, den Assembler-Code des OP-Codes mit dem Assembler-Code des folgenden Codes zu vergleichen, ohne undefiniertes Verhalten.
Tatsächlich hat der richtige Code eine Beendigungsbedingung.
Beschäftige dich damit, du hast den Buggy-Code geschrieben und solltest dich schlecht fühlen. Trage die Konsequenzen.
... oder alternativ bessere Diagnose- und Debugging-Tools richtig einsetzen - dafür sind sie gedacht:
Aktivieren Sie alle Warnungen
-Wall
ist die Option gcc, die alle nützlichen Warnungen ohne Fehlalarme aktiviert. Dies ist ein absolutes Minimum, das Sie immer verwenden sollten.-Wall
da sie möglicherweise bei Fehlalarmen warnenVerwenden Sie Debug-Flags zum Debuggen
-ftrapv
fängt das Programm den Überlauf ein,-fcatch-undefined-behavior
fängt eine Menge Fälle von nicht definiertes Verhalten (Anmerkung:"a lot of" != "all of them"
)Verwenden Sie gcc's
-fwrapv
1 - Diese Regel gilt nicht für "vorzeichenlosen Ganzzahlüberlauf", wie in §3.9.1.4 angegeben
und zB ist das Ergebnis von
UINT_MAX + 1
mathematisch definiert - nach den Regeln des arithmetischen Modulo 2 nquelle
i
selbst betroffen? Im Allgemeinen hat undefiniertes Verhalten nicht diese Art von seltsamen Nebenwirkungen, schließlichi*100000000
sollte ein Wert seini
einem Wert größer als 2 ein undefiniertes Verhalten hat -> (2) können wir annehmen, dassi <= 2
für Optimierungszwecke -> (3) die Schleifenbedingung immer wahr ist -> (4 ) Es ist in eine Endlosschleife optimiert.i
ausgibt , der stattdessen bei jeder Iteration um 1e9 erhöht wird (und die Schleifenbedingung entsprechend ändert). Dies ist eine vollkommen gültige Optimierung unter der "als ob" -Regel, da dieses Programm den Unterschied nicht beobachten konnte, wenn es sich gut benahm. Leider nicht, und die Optimierung "leckt".Eine kurze Antwort,
gcc
die dieses Problem speziell dokumentiert hat, können wir in den Versionshinweisen zu gcc 4.8 sehen, in denen es heißt ( Hervorhebung meiner in Zukunft ):und in der Tat, wenn wir
-fno-aggressive-loop-optimizations
die Endlosschleife verwenden, sollte das Verhalten aufhören und dies in allen Fällen, die ich getestet habe.Die lange Antwort beginnt mit dem Wissen, dass ein Überlauf mit vorzeichenbehafteten Ganzzahlen ein undefiniertes Verhalten ist, indem Sie sich den Entwurf des C ++ - Standardabschnitts ansehen.
5
Ausdrücke Absatz 4 lautet:Wir wissen, dass der Standard besagt, dass undefiniertes Verhalten aus der Anmerkung, die mit der Definition geliefert wird, die besagt:
Aber was in aller Welt kann der
gcc
Optimierer tun, um daraus eine Endlosschleife zu machen? Es klingt völlig verrückt. Aber zum Glückgcc
gibt uns ein Hinweis, wie wir es in der Warnung herausfinden können:Der Hinweis ist
Waggressive-loop-optimizations
, was bedeutet das? Zum Glück ist dies nicht das erste Mal, dass diese Optimierung den Code auf diese Weise beschädigt hat , und wir haben Glück, dass John Regehr einen Fall in dem Artikel GCC vor 4.8 Breaks Broken SPEC 2006 Benchmarks dokumentiert hat, der den folgenden Code zeigt:Der Artikel sagt:
und später sagt:
In einigen Fällen muss der Compiler also davon ausgehen, dass ein vordefinierter Ganzzahlüberlauf ein undefiniertes Verhalten ist und dann
i
immer kleiner sein muss,4
sodass wir eine Endlosschleife haben.Er erklärt, dass dies dem berüchtigten Entfernen von Nullzeigerprüfungen im Linux-Kernel sehr ähnlich ist, wenn man diesen Code sieht:
gcc
gefolgert, dass das
ins->f;
und da dereferenziert ein Nullzeiger ein undefiniertes Verhaltens
ist, nicht null sein darf und daher dieif (!s)
Prüfung in der nächsten Zeile optimiert wird.Die Lehre hier ist, dass moderne Optimierer sehr aggressiv gegenüber undefiniertem Verhalten sind und höchstwahrscheinlich nur aggressiver werden. Mit nur wenigen Beispielen können wir klar erkennen, dass der Optimierer Dinge tut, die für einen Programmierer völlig unvernünftig erscheinen, aber im Nachhinein aus Sicht des Optimierers sinnvoll sind.
quelle
tl; dr Der Code generiert einen Test, der Ganzzahl + positive Ganzzahl == negative Ganzzahl . Normalerweise optimiert der Optimierer dies nicht, aber in dem speziellen Fall,
std::endl
dass er als nächstes verwendet wird, optimiert der Compiler diesen Test. Ich habe noch nicht herausgefunden, was das Besondere istendl
.Aus dem Assembler-Code auf -O1 und höheren Ebenen geht hervor, dass gcc die Schleife auf Folgendes umgestaltet:
Der größte Wert, der korrekt funktioniert, ist
715827882
floor (INT_MAX/3
). Das Assembly-Snippet-O1
lautet:Hinweis : das
-1431655768
ist4 * 715827882
im 2er-Komplement.Durch Schlagen wird dies
-O2
auf Folgendes optimiert:Die vorgenommene Optimierung besteht also lediglich darin, dass die
addl
höher verschoben wurde.Wenn wir
715827883
stattdessen mit neu kompilieren, ist die -O1-Version bis auf die geänderte Anzahl und den geänderten Testwert identisch. -O2 nimmt dann jedoch eine Änderung vor:Wo es
cmpl $-1431655764, %esi
an-O1
, hat diese Linie für entfernt worden-O2
. Der Optimierer muss entschieden haben, dass das Hinzufügen715827883
zu%esi
niemals gleich sein kann-1431655764
.Das ist ziemlich rätselhaft. Wenn Sie
INT_MIN+1
dies hinzufügen, wird das erwartete Ergebnis generiert. Der Optimierer muss also entschieden haben, dass%esi
dies niemals möglich ist,INT_MIN+1
und ich bin mir nicht sicher, warum er dies entscheiden würde.Im Arbeitsbeispiel scheint es gleichermaßen gültig zu sein, zu dem Schluss zu kommen, dass das Hinzufügen
715827882
zu einer Zahl nicht gleich sein kannINT_MIN + 715827882 - 2
! (Dies ist nur möglich, wenn tatsächlich ein Wraparound auftritt), optimiert jedoch nicht die Linie in diesem Beispiel.Der Code, den ich verwendet habe, ist:
Wenn das
std::endl(std::cout)
entfernt wird, erfolgt die Optimierung nicht mehr. Das Ersetzen durch führtstd::cout.put('\n'); std::flush(std::cout);
auch dazu, dass die Optimierung nicht stattfindet, obwohl siestd::endl
inline ist.Das Inlining von
std::endl
scheint den früheren Teil der Schleifenstruktur zu beeinflussen (was ich nicht ganz verstehe, was es tut, aber ich werde es hier posten, falls es jemand anderes tut):Mit Originalcode und
-O2
:Mit mymanual inlining von
std::endl
,-O2
:Ein Unterschied zwischen diesen beiden besteht darin, dass
%esi
sie im Original und%ebx
in der zweiten Version verwendet werden. Gibt es einen Unterschied in der Semantik, der zwischen%esi
und%ebx
allgemein definiert ist? (Ich weiß nicht viel über x86-Assembly).quelle
Ein weiteres Beispiel für diesen Fehler, der in gcc gemeldet wird, ist, wenn Sie eine Schleife haben, die für eine konstante Anzahl von Iterationen ausgeführt wird, die Zählervariable jedoch als Index für ein Array verwenden, das weniger als diese Anzahl von Elementen enthält, z.
Der Compiler kann bestimmen, dass diese Schleife versucht, auf Speicher außerhalb des Arrays 'a' zuzugreifen. Der Compiler beschwert sich darüber mit dieser eher kryptischen Nachricht:
quelle
Es scheint, dass ein ganzzahliger Überlauf in der 4. Iteration (für
i = 3
) auftritt .signed
Ein ganzzahliger Überlauf ruft undefiniertes Verhalten hervor . In diesem Fall kann nichts vorhergesagt werden. Die Schleife kann nur4
mal iterieren oder unendlich werden oder irgendetwas anderes!Das Ergebnis kann von Compiler zu Compiler oder sogar für verschiedene Versionen desselben Compilers variieren.
C11: 1.3.24 undefiniertes Verhalten:
quelle