Der folgende Code geht auf GCC in eine Endlosschleife:
#include <iostream>
using namespace std;
int main(){
int i = 0x10000000;
int c = 0;
do{
c++;
i += i;
cout << i << endl;
}while (i > 0);
cout << c << endl;
return 0;
}
Also hier ist der Deal: Signierter Integer-Überlauf ist technisch undefiniertes Verhalten. GCC auf x86 implementiert jedoch eine Ganzzahlarithmetik unter Verwendung von x86-Ganzzahlanweisungen, die den Überlauf umbrechen.
Daher hätte ich erwartet, dass es sich um einen Überlauf handelt - trotz der Tatsache, dass es sich um ein undefiniertes Verhalten handelt. Das ist aber eindeutig nicht der Fall. So ... Was habe ich verpasst?
Ich habe dies zusammengestellt mit:
~/Desktop$ g++ main.cpp -O2
GCC-Ausgabe:
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
0
0
0
... (infinite loop)
Bei deaktivierten Optimierungen gibt es keine Endlosschleife und die Ausgabe ist korrekt. Visual Studio kompiliert dies ebenfalls korrekt und liefert das folgende Ergebnis:
Richtige Ausgabe:
~/Desktop$ g++ main.cpp
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
3
Hier sind einige andere Variationen:
i *= 2; // Also fails and goes into infinite loop.
i <<= 1; // This seems okay. It does not enter infinite loop.
Hier sind alle relevanten Versionsinformationen:
~/Desktop$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/x86_64-linux-gnu/gcc/x86_64-linux-gnu/4.5.2/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ..
...
Thread model: posix
gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu4)
~/Desktop$
Die Frage ist also: Ist das ein Fehler in GCC? Oder habe ich etwas falsch verstanden, wie GCC mit Ganzzahlarithmetik umgeht?
* Ich markiere auch dieses C, da ich davon ausgehe, dass sich dieser Fehler in C reproduziert. (Ich habe ihn noch nicht überprüft.)
BEARBEITEN:
Hier ist die Zusammenstellung der Schleife: (wenn ich sie richtig erkannt habe)
.L5:
addl %ebp, %ebp
movl $_ZSt4cout, %edi
movl %ebp, %esi
.cfi_offset 3, -40
call _ZNSolsEi
movq %rax, %rbx
movq (%rax), %rax
movq -24(%rax), %rax
movq 240(%rbx,%rax), %r13
testq %r13, %r13
je .L10
cmpb $0, 56(%r13)
je .L3
movzbl 67(%r13), %eax
.L4:
movsbl %al, %esi
movq %rbx, %rdi
addl $1, %r12d
call _ZNSo3putEc
movq %rax, %rdi
call _ZNSo5flushEv
cmpl $3, %r12d
jne .L5
gcc -S
.Antworten:
Wenn der Standard sagt, dass es sich um undefiniertes Verhalten handelt, bedeutet dies es . Alles kann passieren. "Alles" beinhaltet "normalerweise ganze Zahlen, aber gelegentlich passieren seltsame Dinge".
Ja, auf x86-CPUs werden Ganzzahlen normalerweise so umbrochen, wie Sie es erwarten. Dies ist eine dieser Ausnahmen. Der Compiler geht davon aus, dass Sie kein undefiniertes Verhalten verursachen, und optimiert den Schleifentest. Wenn Sie wirklich eine Umgehung wünschen, übergeben Sie diese
-fwrapv
ang++
odergcc
beim Kompilieren. Dies gibt Ihnen eine genau definierte Überlaufsemantik (Zweierkomplement), kann jedoch die Leistung beeinträchtigen.quelle
-fwrapv
. Vielen Dank für den Hinweis.Es ist ganz einfach: Undefiniertes Verhalten - insbesondere bei aktivierter Optimierung (
-O2
) - bedeutet, dass alles passieren kann.Ihr Code verhält sich wie erwartet ohne den
-O2
Schalter.Es funktioniert übrigens ganz gut mit icl und tcc, aber man kann sich nicht auf solche Sachen verlassen ...
Nach diesem , gcc Optimierungs Exploits Integer - Überlauf tatsächlich unterzeichnet. Dies würde bedeuten, dass der "Fehler" beabsichtigt ist.
quelle
for (j = i; j < i + 10; ++j) ++k;
wird beispielsweise nur festgelegtk = 10
, da dies immer dann zutrifft, wenn kein signierter Überlauf auftritt.Hierbei ist zu beachten, dass C ++ - Programme für die abstrakte C ++ - Maschine geschrieben werden (die normalerweise über Hardwareanweisungen emuliert wird). Die Tatsache, dass Sie für x86 kompilieren, ist für die Tatsache, dass dies ein undefiniertes Verhalten aufweist, völlig irrelevant.
Dem Compiler steht es frei, das Vorhandensein von undefiniertem Verhalten zu verwenden, um seine Optimierungen zu verbessern (indem eine Bedingung wie in diesem Beispiel aus einer Schleife entfernt wird). Es gibt keine garantierte oder sogar nützliche Zuordnung zwischen Konstrukten auf C ++ - Ebene und Maschinencodekonstrukten auf x86-Ebene, abgesehen von der Anforderung, dass der Maschinencode bei seiner Ausführung das von der abstrakten C ++ - Maschine geforderte Ergebnis liefert.
quelle
// Der Überlauf ist undefiniert.
Mit -fwrapv ist es richtig. -fwrapv
quelle
Bitte Leute, undefiniertes Verhalten ist genau das, undefiniert . Es bedeutet, dass alles passieren könnte. In der Praxis (wie in diesem Fall) kann der Compiler davon ausgehen, dass dies nicht der Fall istaufgerufen werden und tun, was immer es will, wenn dies den Code schneller / kleiner machen könnte. Was mit Code passiert, der nicht ausgeführt werden sollte, ist unklar. Dies hängt vom umgebenden Code ab (abhängig davon kann der Compiler durchaus unterschiedlichen Code generieren), den verwendeten Variablen / Konstanten, den Compiler-Flags, ... Oh, und der Compiler könnte aktualisiert werden und denselben Code anders schreiben, oder Sie könnten Holen Sie sich einen anderen Compiler mit einer anderen Sicht auf die Codegenerierung. Oder holen Sie sich einfach eine andere Maschine, sogar ein anderes Modell in derselben Architekturlinie könnte sehr wohl ein eigenes undefiniertes Verhalten haben (suchen Sie nach undefinierten Opcodes, einige unternehmungslustige Programmierer fanden heraus, dass einige dieser frühen Maschinen manchmal nützliche Dinge taten ...) . Es gibt keine"Der Compiler gibt ein bestimmtes Verhalten bei undefiniertem Verhalten an". Es gibt Bereiche, die implementierungsdefiniert sind, und dort sollten Sie sich darauf verlassen können, dass sich der Compiler konsistent verhält.
quelle
Selbst wenn ein Compiler angeben würde, dass ein Ganzzahlüberlauf als "unkritische" Form des undefinierten Verhaltens (wie in Anhang L definiert) betrachtet werden muss, sollte das Ergebnis eines Ganzzahlüberlaufs ohne ein spezifisches Plattformversprechen eines spezifischeren Verhaltens vorliegen mindestens als "teilweise unbestimmter Wert" angesehen. Nach solchen Regeln könnte das Hinzufügen von 1073741824 + 1073741824 willkürlich als Ergebnis von 2147483648 oder -2147483648 oder einem anderen Wert angesehen werden, der mit 2147483648 mod 4294967296 kongruent ist, und durch Addition erhaltene Werte können willkürlich als jeder Wert angesehen werden, der mit 0 mod 4294967296 kongruent ist.
Regeln, die es einem Überlauf ermöglichen, "teilweise unbestimmte Werte" zu erhalten, wären hinreichend genau definiert, um den Buchstaben und den Geist von Anhang L einzuhalten, würden jedoch einen Compiler nicht daran hindern, dieselben allgemein nützlichen Schlussfolgerungen zu ziehen, die gerechtfertigt wären, wenn Überläufe nicht eingeschränkt wären Undefiniertes Verhalten. Dies würde einen Compiler daran hindern, falsche "Optimierungen" vorzunehmen, deren Hauptwirkung in vielen Fällen darin besteht, dass Programmierer dem Code zusätzliche Unordnung hinzufügen müssen, dessen einziger Zweck darin besteht, solche "Optimierungen" zu verhindern. ob das gut wäre oder nicht, hängt vom eigenen Standpunkt ab.
quelle