Warum erzeugt diese Schleife "Warnung: Iteration 3u ruft undefiniertes Verhalten auf" und gibt mehr als 4 Zeilen aus?

162

Zusammenstellung:

#include <iostream>

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

und gccerzeugt 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 iWert 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)

zerkms
quelle
49
Signierter ganzzahliger Überlauf => Undefiniertes Verhalten => Nasendämonen. Aber ich muss zugeben, dieses Beispiel ist ganz nett.
Dyp
1
Versammlungsausgabe: goo.gl/TtPmZn
Bryan Chen
1
Passiert auf GCC 4.8 mit der O2, und O3Flagge, aber nicht O0oderO1
Alex
3
@dyp Als ich Nasal Daemons las, habe ich das "Imgur-Lachen" gemacht, das darin besteht, die Nase leicht auszuatmen, wenn man etwas Lustiges sieht. Und dann wurde mir klar ... ich muss von einem Nasendämon verflucht werden!
CorsiKa
4
Lesezeichen setzen, damit ich es das nächste Mal verlinken kann, wenn jemand erwidert "es ist technisch UB, aber es sollte etwas tun " :)
MM

Antworten:

107

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

Wenn während der Auswertung eines Ausdrucks das Ergebnis nicht mathematisch definiert ist oder nicht im Bereich der darstellbaren Werte für seinen Typ liegt, ist das Verhalten nicht definiert. [Hinweis: Die meisten vorhandenen Implementierungen von C ++ ignorieren ganzzahlige Überläufe. Die Behandlung der Division durch Null, die Bildung eines Restes unter Verwendung eines Nullteilers und aller Fließkomma-Ausnahmen variiert zwischen den Maschinen und ist normalerweise durch eine Bibliotheksfunktion einstellbar. - Endnote]

Ihr mit kompilierter Code g++ -O3gibt Warnung aus (auch ohne -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)
  ^

Die einzige Möglichkeit, zu analysieren, was das Programm tut, besteht darin, den generierten Assemblycode zu lesen.

Hier ist die vollständige Baugruppenliste:

    .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

Ich kann kaum Baugruppen lesen, aber selbst ich kann die addl $1000000000, %ediLinie sehen. Der resultierende Code sieht eher so aus

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

Dieser Kommentar von @TC:

Ich vermute, dass es so etwas wie: (1) weil jede Iteration mit ieinem Wert größer als 2 ein undefiniertes Verhalten hat -> (2) können wir annehmen, dass i <= 2für Optimierungszwecke -> (3) die Schleifenbedingung immer wahr ist -> (4 ) Es ist in eine Endlosschleife optimiert.

gab mir die Idee, den Assembler-Code des OP-Codes mit dem Assembler-Code des folgenden Codes zu vergleichen, ohne undefiniertes Verhalten.

#include <iostream>

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

Tatsächlich hat der richtige Code eine Beendigungsbedingung.

    ; ...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, das ist völlig nicht klar! Das ist nicht fair! Ich fordere eine Feuerprobe!

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

    • -Wallist die Option gcc, die alle nützlichen Warnungen ohne Fehlalarme aktiviert. Dies ist ein absolutes Minimum, das Sie immer verwenden sollten.
    • gcc verfügt über viele andere Warnoptionen , die jedoch nicht aktiviert sind, -Wallda sie möglicherweise bei Fehlalarmen warnen
    • Visual C ++ bleibt leider mit der Fähigkeit, nützliche Warnungen zu geben, zurück. Zumindest die IDE aktiviert einige standardmäßig.
  • Verwenden Sie Debug-Flags zum Debuggen

    • für einen ganzzahligen Überlauf -ftrapvfängt das Programm den Überlauf ein,
    • Clang - Compiler ist hervorragend für diese: -fcatch-undefined-behaviorfängt eine Menge Fälle von nicht definiertes Verhalten (Anmerkung: "a lot of" != "all of them")

Ich habe ein Spaghetti-Durcheinander eines Programms, das nicht von mir geschrieben wurde und morgen versendet werden muss! HILFE !!!!!!

Verwenden Sie gcc's -fwrapv

Diese Option weist den Compiler an, anzunehmen, dass der vorzeichenbehaftete arithmetische Überlauf von Addition, Subtraktion und Multiplikation mithilfe der Zweierkomplementdarstellung umgangen wird.

1 - Diese Regel gilt nicht für "vorzeichenlosen Ganzzahlüberlauf", wie in §3.9.1.4 angegeben

Ganzzahlen ohne Vorzeichen, die als vorzeichenlos deklariert sind, müssen den Gesetzen des arithmetischen Modulos 2 n entsprechen, wobei n die Anzahl der Bits in der Wertdarstellung dieser bestimmten Größe von Ganzzahlen ist.

und zB ist das Ergebnis von UINT_MAX + 1mathematisch definiert - nach den Regeln des arithmetischen Modulo 2 n

Milleniumbug
quelle
7
Ich verstehe immer noch nicht wirklich, was hier passiert ... Warum ist es iselbst betroffen? Im Allgemeinen hat undefiniertes Verhalten nicht diese Art von seltsamen Nebenwirkungen, schließlich i*100000000sollte ein Wert sein
vsoftco
26
Ich vermute, dass es so etwas wie: (1) weil jede Iteration mit ieinem Wert größer als 2 ein undefiniertes Verhalten hat -> (2) können wir annehmen, dass i <= 2für Optimierungszwecke -> (3) die Schleifenbedingung immer wahr ist -> (4 ) Es ist in eine Endlosschleife optimiert.
TC
28
@vsoftco: Was passiert, ist ein Fall der Festigkeitsreduzierung , genauer gesagt der Eliminierung von Induktionsvariablen . Der Compiler eliminiert die Multiplikation, indem er Code iausgibt , 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".
JohannesD
8
@JohannesD hat den Grund dafür gefunden. Dies ist jedoch eine schlechte Optimierung, da die Schleifenbeendigungsbedingung keinen Überlauf beinhaltet. Die Verwendung der Festigkeitsreduzierung war in Ordnung - ich weiß nicht, was der Multiplikator im Prozessor mit (4 * 100000000) tun würde, der sich von (100000000 + 100000000 + 100000000 + 100000000) unterscheidet, und auf "es ist undefiniert" zurückgreifen - wer weiß "ist vernünftig. Ersetzen Sie jedoch eine gut erzogene Schleife, die viermal ausgeführt wird und undefinierte Ergebnisse liefert, durch etwas, das mehr als viermal ausgeführt wird, "weil es undefiniert ist!" ist Idiotie.
Julie in Austin
14
@ JulieinAustin Während es für Sie idiotisch sein mag, ist es vollkommen legal. Positiv zu vermerken ist, dass der Compiler Sie davor warnt.
Milleniumbug
68

Eine kurze Antwort, gccdie 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 ):

GCC verwendet jetzt eine aggressivere Analyse, um eine Obergrenze für die Anzahl der Iterationen von Schleifen unter Verwendung von durch Sprachstandards auferlegten Einschränkungen abzuleiten . Dies kann dazu führen, dass nicht konforme Programme nicht mehr wie erwartet funktionieren, z. B. SPEC CPU 2006 464.h264ref und 416.gamess. Eine neue Option, -fno-aggressive-loop-Optimierungen, wurde hinzugefügt, um diese aggressive Analyse zu deaktivieren. In einigen Schleifen, bei denen eine konstante Anzahl von Iterationen bekannt ist, von denen jedoch bekannt ist, dass undefiniertes Verhalten in der Schleife vor Erreichen oder während der letzten Iteration auftritt, warnt GCC vor dem undefinierten Verhalten in der Schleife, anstatt die untere Obergrenze der Anzahl von Iterationen abzuleiten für die Schleife. Die Warnung kann mit -Wno-Aggressive-Loop-Optimierungen deaktiviert werden.

und in der Tat, wenn wir -fno-aggressive-loop-optimizationsdie 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:

Wenn während der Auswertung eines Ausdrucks das Ergebnis nicht mathematisch definiert ist oder nicht im Bereich der darstellbaren Werte für seinen Typ liegt, ist das Verhalten undefiniert . [Hinweis: Die meisten vorhandenen Implementierungen von C ++ ignorieren Ganzzahlüberläufe. Die Behandlung der Division durch Null, die Bildung eines Restes unter Verwendung eines Nullteilers und aller Gleitkommaausnahmen variiert zwischen den Maschinen und ist normalerweise durch eine Bibliotheksfunktion einstellbar. - Endnote

Wir wissen, dass der Standard besagt, dass undefiniertes Verhalten aus der Anmerkung, die mit der Definition geliefert wird, die besagt:

[Hinweis: Undefiniertes Verhalten kann erwartet werden, wenn diese Internationale Norm keine explizite Definition des Verhaltens enthält oder wenn ein Programm ein fehlerhaftes Konstrukt oder fehlerhafte Daten verwendet. Das zulässige undefinierte Verhalten reicht vom vollständigen Ignorieren der Situation mit unvorhersehbaren Ergebnissen über das Verhalten während der Übersetzung oder Programmausführung in einer für die Umgebung charakteristischen dokumentierten Weise (mit oder ohne Ausgabe einer Diagnosemeldung) bis zum Beenden einer Übersetzung oder Ausführung (mit der Ausgabe) einer Diagnosemeldung). Viele fehlerhafte Programmkonstrukte erzeugen kein undefiniertes Verhalten. Sie müssen diagnostiziert werden. - Endnote]

Aber was in aller Welt kann der gccOptimierer tun, um daraus eine Endlosschleife zu machen? Es klingt völlig verrückt. Aber zum Glück gccgibt uns ein Hinweis, wie wir es in der Warnung herausfinden können:

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

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:

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;
}

Der Artikel sagt:

Das undefinierte Verhalten greift kurz vor dem Verlassen der Schleife auf d [16] zu. In C99 ist es zulässig, einen Zeiger auf ein Element eine Position nach dem Ende des Arrays zu erstellen. Dieser Zeiger darf jedoch nicht dereferenziert werden.

und später sagt:

Im Detail ist hier, was los ist. Wenn der AC-Compiler d [++ k] sieht, darf er davon ausgehen, dass der inkrementierte Wert von k innerhalb der Array-Grenzen liegt, da ansonsten undefiniertes Verhalten auftritt. Für den Code hier kann GCC schließen, dass k im Bereich 0..15 liegt. Etwas später, wenn GCC k <16 sieht, sagt es sich: "Aha - dieser Ausdruck ist immer wahr, also haben wir eine Endlosschleife." Die Situation hier, in der der Compiler die Annahme einer genauen Definition verwendet, um auf eine nützliche Datenfluss-Tatsache zu schließen,

In einigen Fällen muss der Compiler also davon ausgehen, dass ein vordefinierter Ganzzahlüberlauf ein undefiniertes Verhalten ist und dann iimmer kleiner sein muss, 4sodass 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:

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

gccgefolgert, dass da sin s->f;und da dereferenziert ein Nullzeiger ein undefiniertes Verhalten sist, nicht null sein darf und daher die if (!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.

Shafik Yaghmour
quelle
7
Ich verstehe, dass dies das ist, was der Compiler-Writer tut (ich habe früher Compiler und sogar ein oder zwei Optimierer geschrieben), aber es gibt Verhaltensweisen, die "nützlich" sind, obwohl sie "undefiniert" sind, und diesen Marsch hin zu einer immer aggressiveren Optimierung ist nur Wahnsinn. Das oben angegebene Konstrukt ist falsch, aber die Optimierung der Fehlerprüfung ist benutzerfeindlich.
Julie in Austin
1
@ JulieinAustin Ich stimme zu, dass dies ein ziemlich überraschendes Verhalten ist. Zu sagen, dass Entwickler undefiniertes Verhalten vermeiden müssen, ist wirklich nur die halbe Miete. Natürlich muss der Compiler auch dem Entwickler ein besseres Feedback geben. In diesem Fall wird eine Warnung ausgegeben, die jedoch nicht informativ genug ist.
Shafik Yaghmour
3
Ich denke, es ist eine gute Sache, ich möchte besseren, schnelleren Code. UB sollte niemals verwendet werden.
Paulm
1
@paulm moralisch UB ist eindeutig schlecht, aber es ist schwer zu argumentieren, bessere, bessere Tools wie den Clang Static Analyzer bereitzustellen, mit denen Entwickler UB und andere Probleme erkennen können, bevor sie sich auf Produktionsanwendungen auswirken.
Shafik Yaghmour
1
@ShafikYaghmour Wenn Ihr Entwickler Warnungen ignoriert, wie hoch sind dann die Chancen, dass er der Clang-Ausgabe Aufmerksamkeit schenkt? Dieses Problem kann leicht durch eine aggressive Richtlinie "Keine ungerechtfertigten Warnungen" behoben werden. Klirren ratsam, aber nicht erforderlich.
Deworde
24

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::endldass er als nächstes verwendet wird, optimiert der Compiler diesen Test. Ich habe noch nicht herausgefunden, was das Besondere ist endl.


Aus dem Assembler-Code auf -O1 und höheren Ebenen geht hervor, dass gcc die Schleife auf Folgendes umgestaltet:

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

Der größte Wert, der korrekt funktioniert, ist 715827882floor ( INT_MAX/3). Das Assembly-Snippet -O1lautet:

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

Hinweis : das -1431655768ist 4 * 715827882im 2er-Komplement.

Durch Schlagen wird dies -O2auf Folgendes optimiert:

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

Die vorgenommene Optimierung besteht also lediglich darin, dass die addlhöher verschoben wurde.

Wenn wir 715827883stattdessen 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:

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

Wo es cmpl $-1431655764, %esian -O1, hat diese Linie für entfernt worden -O2. Der Optimierer muss entschieden haben, dass das Hinzufügen 715827883zu %esiniemals 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 %esidies niemals möglich ist, INT_MIN+1und 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 715827882zu einer Zahl nicht gleich sein kann INT_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:

#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);
    }
}

Wenn das std::endl(std::cout)entfernt wird, erfolgt die Optimierung nicht mehr. Das Ersetzen durch führt std::cout.put('\n'); std::flush(std::cout);auch dazu, dass die Optimierung nicht stattfindet, obwohl sie std::endlinline ist.

Das Inlining von std::endlscheint 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:

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

Mit mymanual inlining von 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

Ein Unterschied zwischen diesen beiden besteht darin, dass %esisie im Original und %ebxin der zweiten Version verwendet werden. Gibt es einen Unterschied in der Semantik, der zwischen %esiund %ebxallgemein definiert ist? (Ich weiß nicht viel über x86-Assembly).

MM
quelle
Es wäre gut herauszufinden, was genau die Logik des Optimierers war. Es ist mir derzeit nicht klar, warum in einigen Fällen der Test optimiert wurde und in anderen nicht
MM
8

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.

int a[50], x;

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

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:

Iteration xxu ruft undefiniertes Verhalten auf [-Werror = Aggressive-Loop-Optimierungen]

Ed Tyler
quelle
Noch kryptischer ist, dass die Nachricht nur ausgegeben wird, wenn die Optimierung aktiviert ist. M $ VB Nachricht "Array out of bound" ist für Dummies?
Ravi Ganesh
6

Was ich nicht bekommen kann ist, warum ich Wert durch diese Überlaufoperation gebrochen wird?

Es scheint, dass ein ganzzahliger Überlauf in der 4. Iteration (für i = 3) auftritt . signedEin ganzzahliger Überlauf ruft undefiniertes Verhalten hervor . In diesem Fall kann nichts vorhergesagt werden. Die Schleife kann nur 4mal 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:

Verhalten, für das diese Internationale Norm keine Anforderungen stellt
[Hinweis: Undefiniertes Verhalten kann erwartet werden, wenn diese Internationale Norm keine explizite Definition des Verhaltens enthält oder wenn ein Programm ein fehlerhaftes Konstrukt oder fehlerhafte Daten verwendet. Das zulässige undefinierte Verhalten reicht vom vollständigen Ignorieren der Situation mit unvorhersehbaren Ergebnissen über das Verhalten während der Übersetzung oder Programmausführung in einer für die Umgebung charakteristischen dokumentierten Weise (mit oder ohne Ausgabe einer Diagnosemeldung) bis zum Beenden einer Übersetzung oder Ausführung (mit der Ausgabe) einer Diagnosemeldung) . Viele fehlerhafte Programmkonstrukte erzeugen kein undefiniertes Verhalten. Sie müssen diagnostiziert werden. - Endnote]

Haccks
quelle
@bits_international; Ja.
Haccks
4
Du hast recht, es ist fair zu erklären, warum ich abgelehnt habe. Die Informationen in dieser Antwort sind korrekt, aber nicht lehrreich und ignorieren den Elefanten im Raum vollständig: Der Bruch tritt anscheinend an einer anderen Stelle (Stoppbedingung) auf als die Operation, die den Überlauf verursacht. Die Mechanik, wie Dinge in diesem speziellen Fall kaputt gehen, wird nicht erklärt, obwohl dies der Kern dieser Frage ist. Es ist eine typische schlechte Lehrersituation, in der die Antwort des Lehrers nicht nur den Kern des Problems nicht anspricht, sondern weitere Fragen entmutigt. Es klingt fast wie ...
Szabolcs
5
"Ich sehe, dass dies undefiniertes Verhalten ist, und von diesem Punkt an ist es mir egal, wie oder warum es bricht. Der Standard erlaubt es zu brechen. Keine weiteren Fragen." Sie haben es vielleicht nicht so gemeint, aber es scheint so. Ich hoffe, weniger von dieser (leider häufigen) Haltung auf SO zu sehen. Dies ist praktisch nicht sinnvoll. Wenn Sie Benutzereingaben erhalten, ist es nicht sinnvoll, nach jeder einzelnen vorzeichenbehafteten Ganzzahloperation auf Überlauf zu prüfen , selbst wenn der Standard besagt, dass ein anderer Teil des Programms aufgrund dessen möglicherweise explodiert. Das Verständnis , wie es bricht tut Hilfe vermeiden Probleme wie diese in der Praxis.
Szabolcs
2
@Szabolcs: Es ist möglicherweise am besten, sich C als zwei Sprachen vorzustellen, von denen eine so konzipiert ist, dass einfache Compiler mit Hilfe von Programmierern, die Konstrukte ausnutzen, die auf ihren beabsichtigten Zielplattformen zuverlässig sind, einen nicht effizienten ausführbaren Code erzielen, aber nicht andere und wurden folglich vom Standardkomitee ignoriert, und eine zweite Sprache, die alle derartigen Konstrukte ausschließt, für die der Standard keine Unterstützung vorschreibt, um es Compilern zu ermöglichen, zusätzliche Optimierungen anzuwenden, die die Programmierer überwiegen können oder nicht Gib auf.
Supercat
1
@Szabolcs " Wenn Sie Benutzereingaben erhalten, ist es nicht sinnvoll, nach jeder einzelnen vorzeichenbehafteten Ganzzahloperation auf Überlauf zu prüfen " - richtig, da es zu diesem Zeitpunkt zu spät ist. Sie müssen vor jeder einzelnen vorzeichenbehafteten Ganzzahloperation auf Überlauf prüfen .
Melpomene