Dlaczego ta pętla generuje „ostrzeżenie: iteracja 3u wywołuje niezdefiniowane zachowanie” i wyświetla więcej niż 4 wiersze?

162

Kompilowanie tego:

#include <iostream>

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

i gccgeneruje następujące ostrzeżenie:

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

Rozumiem, że występuje przepełnienie liczby całkowitej ze znakiem.

Nie mogę zrozumieć, dlaczego ita operacja przepełnienia powoduje zerwanie wartości?

Przeczytałem odpowiedzi na pytanie Dlaczego przepełnienie całkowitoliczbowe na x86 z GCC powoduje nieskończoną pętlę? , ale nadal nie jestem pewien, dlaczego tak się dzieje - rozumiem, że „nieokreślony” oznacza „wszystko może się zdarzyć”, ale jaka jest przyczyna tego konkretnego zachowania ?

Online: http://ideone.com/dMrRKR

Kompilator: gcc (4.8)

zerkms
źródło
49
Przepełnienie liczby całkowitej ze znakiem => Niezdefiniowane zachowanie => Demony nosowe. Ale muszę przyznać, że ten przykład jest całkiem niezły.
dyp
1
Wynik
Bryan Chen
1
Dzieje się na GCC 4.8 z flagą O2i O3, ale nie O0lubO1
Alex
3
@dyp, kiedy czytałem Demony nosowe, zrobiłem „imgur śmiech”, który polega na lekkim wydychaniu nosa, gdy widzisz coś zabawnego. I wtedy zdałem sobie sprawę ... Muszę zostać przeklęty przez demona nosa!
corsiKa
4
Zakładki tego, aby można było połączyć go następnym razem ktoś retort „To technicznie UB ale powinien zrobić coś ” :)
MM

Odpowiedzi:

107

Przepełnienie liczby całkowitej ze znakiem (a ściślej mówiąc, nie ma czegoś takiego jak „przepełnienie liczby całkowitej bez znaku”) oznacza niezdefiniowane zachowanie . A to oznacza, że ​​wszystko może się zdarzyć, a dyskutowanie, dlaczego tak się dzieje na zasadach C ++, nie ma sensu.

Wersja robocza C ++ 11 N3337: §5.4: 1

Jeżeli podczas oceny wyrażenia wynik nie jest zdefiniowany matematycznie lub nie mieści się w zakresie reprezentowalnych wartości dla jego typu, zachowanie jest niezdefiniowane. [Uwaga: większość istniejących implementacji C ++ ignoruje liczby całkowite po przepływie. Postępowanie z dzieleniem przez zero, tworzeniem reszty za pomocą dzielnika zerowego i wszystkimi wyjątkami dotyczącymi punktu zmiennego są różne w różnych maszynach i zwykle można je regulować za pomocą funkcji bibliotecznej. —End note]

Twój kod skompilowany z g++ -O3emituje ostrzeżenie (nawet bez -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)
  ^

Jedynym sposobem, w jaki możemy przeanalizować, co robi program, jest odczytanie wygenerowanego kodu asemblera.

Oto pełna lista montażu:

    .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

Ledwo mogę czytać montaż, ale nawet ja widzę addl $1000000000, %edilinię. Wynikowy kod wygląda bardziej jak

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

Ten komentarz @TC:

Podejrzewam, że to coś takiego: (1) ponieważ każda iteracja io dowolnej wartości większej niż 2 ma niezdefiniowane zachowanie -> (2) możemy założyć, że i <= 2dla celów optymalizacyjnych -> (3) warunek pętli jest zawsze prawdziwy -> (4 ) jest zoptymalizowany w nieskończoną pętlę.

dał mi pomysł porównania kodu assemblera kodu OP z kodem assemblera następującego kodu, bez niezdefiniowanego zachowania.

#include <iostream>

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

W rzeczywistości poprawny kod ma warunek zakończenia.

    ; ...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, to zupełnie nie jest oczywiste! To niesprawiedliwe! Żądam próby ognia!

Skorzystaj z tego, napisałeś błędny kod i powinieneś czuć się źle. Ponieść konsekwencje.

... lub alternatywnie, odpowiednio wykorzystaj lepszą diagnostykę i lepsze narzędzia do debugowania - do tego służą:

  • włącz wszystkie ostrzeżenia

    • -Wallto opcja gcc, która włącza wszystkie przydatne ostrzeżenia bez fałszywych alarmów. To absolutne minimum, którego zawsze powinieneś używać.
    • gcc ma wiele innych opcji ostrzegawczych , jednak nie są one włączone, -Wallponieważ mogą ostrzegać przed fałszywymi alarmami
    • Visual C ++ niestety pozostaje w tyle, jeśli chodzi o możliwość wyświetlania przydatnych ostrzeżeń. Przynajmniej IDE włącza niektóre domyślnie.
  • użyj flag debugowania do debugowania

    • dla przepełnienia całkowitoliczbowego -ftrapvzatrzymuje program na przepełnieniu,
    • Dzyń kompilator jest doskonała do tego: -fcatch-undefined-behaviorłapie dużo przypadków zachowanie niezdefiniowane (uwaga: "a lot of" != "all of them")

Mam bałagan w programie spaghetti programu, którego nie napisałem, który ma zostać wysłany jutro! POMOCY !!!!!! 111oneone

Użyj gcc -fwrapv

Ta opcja instruuje kompilator, aby założył, że przepełnienie arytmetyczne ze znakiem dodawania, odejmowania i mnożenia zawija się przy użyciu reprezentacji uzupełnień do dwóch.

1 - ta zasada nie ma zastosowania do „przepełnienia liczby całkowitej bez znaku”, zgodnie z §3.9.1.4

Liczby całkowite bez znaku, zadeklarowane bez znaku, powinny być zgodne z prawami arytmetycznej modulo 2 n, gdzie n jest liczbą bitów w reprezentacji wartości danego rozmiaru liczby całkowitej.

i np. wynik UINT_MAX + 1jest określony matematycznie - przez reguły arytmetyczne modulo 2 n

milleniumbug
źródło
7
Nadal nie rozumiem, co się tutaj dzieje ... Dlaczego to idotyczy? Ogólnie niezdefiniowane zachowanie nie ma takich dziwnych skutków ubocznych, w końcu i*100000000powinno być rvalue
vsoftco
26
Podejrzewam, że to coś takiego: (1) ponieważ każda iteracja io dowolnej wartości większej niż 2 ma niezdefiniowane zachowanie -> (2) możemy założyć, że i <= 2dla celów optymalizacyjnych -> (3) warunek pętli jest zawsze prawdziwy -> (4 ) jest zoptymalizowany w nieskończoną pętlę.
TC
28
@vsoftco: To, co się dzieje, to przypadek zmniejszenia siły , a dokładniej eliminacja zmiennej indukcyjnej . Kompilator eliminuje mnożenie, emitując kod, który zamiast tego zwiększa się io 1e9 w każdej iteracji (i odpowiednio zmienia warunek pętli). Jest to całkowicie poprawna optymalizacja w ramach zasady „jak gdyby”, ponieważ ten program nie mógł zauważyć różnicy, czy dobrze się zachowuje. Niestety, tak nie jest i optymalizacja „przecieka”.
JohannesD
8
@JohannesD określił powód tego zerwania. Jest to jednak zła optymalizacja, ponieważ warunek zakończenia pętli nie obejmuje przepełnienia. Użycie redukcji siły było w porządku - nie wiem, co zrobiłby mnożnik w procesorze z (4 * 100000000), który byłby inny z (100000000 + 100000000 + 100000000 + 100000000) i wracając do "to jest niezdefiniowane - kto wie ”jest rozsądne. Ale zastąpienie czegoś, co powinno być dobrze zachowaną pętlą, która wykonuje 4 razy i daje niezdefiniowane wyniki, czymś, co wykonuje więcej niż 4 razy, „ponieważ jest niezdefiniowane!” jest idiotyzmem.
Julie w Austin
14
@JulieinAustin Chociaż może to być dla ciebie idiotyczne, jest całkowicie legalne. Z drugiej strony kompilator ostrzega o tym.
milleniumbug
68

Krótka odpowiedź, gcckonkretnie udokumentowała ten problem, widzimy, że w informacjach o wydaniu gcc 4.8, które mówi ( podkreślenie moje w przyszłości ):

GCC używa teraz bardziej agresywnej analizy, aby wyznaczyć górną granicę liczby iteracji pętli przy użyciu ograniczeń narzuconych przez standardy językowe . Może to spowodować, że niezgodne programy, takie jak SPEC CPU 2006 464.h264ref i 416.gamess, przestaną działać zgodnie z oczekiwaniami. Dodano nową opcję -fno-aggressive-loop-optimizations, aby wyłączyć tę agresywną analizę. W niektórych pętlach, które znają stałą liczbę iteracji, ale wiadomo, że w pętli występuje niezdefiniowane zachowanie przed osiągnięciem lub podczas ostatniej iteracji, GCC ostrzega o niezdefiniowanym zachowaniu w pętli zamiast wyprowadzać dolną górną granicę liczby iteracji na pętlę. Ostrzeżenie można wyłączyć za pomocą opcji -Wno-aggressive-loop-optimizations.

i rzeczywiście, jeśli używamy -fno-aggressive-loop-optimizations nieskończonej pętli, zachowanie powinno ustać i tak jest we wszystkich testowanych przeze mnie przypadkach.

Długa odpowiedź zaczyna się od wiedzy, że przepełnienie liczby całkowitej ze znakiem jest niezdefiniowanym zachowaniem, patrząc na szkic standardowej sekcji C ++ w paragrafie 4 5 Wyrażenia, który mówi:

Jeśli podczas oceny wyrażenia wynik nie jest matematycznie zdefiniowany lub nie mieści się w zakresie reprezentowalnych wartości dla jego typu, zachowanie jest niezdefiniowane . [Uwaga: większość istniejących implementacji C ++ ignoruje przepełnienia całkowitoliczbowe. Postępowanie z dzieleniem przez zero, tworzeniem reszty za pomocą dzielnika zerowego oraz wszystkie wyjątki zmiennoprzecinkowe różnią się w zależności od maszyn i zwykle jest regulowane przez funkcję biblioteczną. - wyślij notatkę

Wiemy, że norma mówi, że niezdefiniowane zachowanie jest nieprzewidywalne na podstawie notatki dołączonej do definicji, która mówi:

[Uwaga: można oczekiwać niezdefiniowanego zachowania, gdy w niniejszej Normie Międzynarodowej pomija się jakąkolwiek wyraźną definicję zachowania lub gdy program używa błędnej konstrukcji lub błędnych danych. Dopuszczalne niezdefiniowane zachowanie waha się od całkowitego zignorowania sytuacji z nieprzewidywalnymi skutkami , poprzez zachowanie podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z lub bez wydania komunikatu diagnostycznego), aż po przerwanie tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego). Wiele błędnych konstrukcji programów nie wywołuje nieokreślonego zachowania; wymagana jest diagnoza. —End note]

Ale co na świecie może zrobić gccoptymalizator, aby przekształcić to w nieskończoną pętlę? Brzmi kompletnie głupio. Ale na szczęście gccdaje nam wskazówkę, jak to rozgryźć w ostrzeżeniu:

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

Chodzi o to Waggressive-loop-optimizations, co to znaczy? Na szczęście dla nas to nie pierwszy raz, kiedy ta optymalizacja zepsuła kod w ten sposób i mamy szczęście, ponieważ John Regehr udokumentował przypadek w artykule GCC pre-4.8 Łamie zepsute testy SPEC 2006, który pokazuje następujący kod:

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

artykuł mówi:

Niezdefiniowane zachowanie polega na dostępie do d [16] tuż przed wyjściem z pętli. W C99 dozwolone jest tworzenie wskaźnika do elementu o jedną pozycję za końcem tablicy, ale wskaźnik ten nie może być wyłuskiwany.

a później mówi:

W szczegółach, oto, co się dzieje. Kompilator AC, widząc d [++ k], może założyć, że zwiększona wartość k mieści się w granicach tablicy, ponieważ w przeciwnym razie zachodzi niezdefiniowane zachowanie. Dla kodu tutaj GCC może wywnioskować, że k jest z zakresu 0..15. Nieco później, gdy GCC widzi k <16, mówi do siebie: „Aha– to wyrażenie jest zawsze prawdziwe, więc mamy nieskończoną pętlę”. Sytuacja tutaj, w której kompilator wykorzystuje założenie dobrze zdefiniowanej, aby wywnioskować przydatny fakt przepływu danych,

Kompilator musi więc w niektórych przypadkach założyć, że przepełnienie ze znakiem całkowitym jest niezdefiniowanym zachowaniem, to izawsze musi być mniejsze, 4a zatem mamy nieskończoną pętlę.

Wyjaśnia, że ​​jest to bardzo podobne do niesławnego usuwania zerowego wskaźnika sprawdzania jądra Linuksa, gdzie widząc ten kod:

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

gccwywnioskowałem, że ponieważ szostał odrzucony w, s->f;a ponieważ dereferencja pustego wskaźnika jest niezdefiniowanym zachowaniem, snie może być zerowa i dlatego optymalizuje if (!s)sprawdzanie w następnej linii.

Lekcja z tego jest taka, że ​​współcześni optymalizatorzy bardzo agresywnie wykorzystują nieokreślone zachowanie i najprawdopodobniej staną się bardziej agresywni. Wystarczy kilka przykładów, aby zobaczyć, że optymalizator robi rzeczy, które wydają się całkowicie nieracjonalne dla programisty, ale z perspektywy optymalizatora mają sens.

Shafik Yaghmour
źródło
7
Rozumiem, że to właśnie robi autor kompilatora (kiedyś pisałem kompilatory, a nawet optymalizator lub dwa), ale są zachowania, które są „przydatne”, mimo że są „nieokreślone”, a ten marsz w kierunku coraz bardziej agresywnej optymalizacji to po prostu szaleństwo. Konstrukcja, którą zacytowałeś powyżej, jest błędna, ale optymalizacja sprawdzania błędów jest wroga dla użytkownika.
Julie w Austin
1
@JulieinAustin Zgadzam się, że jest to dość zaskakujące zachowanie, twierdząc, że programiści muszą unikać niezdefiniowanego zachowania, to tak naprawdę tylko połowa problemu. Oczywiście kompilator musi również zapewnić lepsze informacje zwrotne programistom. W takim przypadku wyświetlane jest ostrzeżenie, chociaż nie jest ono wystarczająco pouczające.
Shafik Yaghmour
3
Myślę, że to dobrze, chcę lepszego, szybszego kodu. Nigdy nie należy używać UB.
paulm
1
@paulm moralnie UB jest wyraźnie zły, ale trudno dyskutować z zapewnieniem lepszych narzędzi, takich jak analizator statyczny clang, aby pomóc programistom wyłapać UB i inne problemy, zanim wpłyną one na aplikacje produkcyjne.
Shafik Yaghmour
1
@ShafikYaghmour Ponadto, jeśli programista ignoruje ostrzeżenia, jakie są szanse, że zwróci uwagę na wyjście dźwięku? Problem ten można łatwo uchwycić dzięki agresywnej polityce „bez nieuzasadnionych ostrzeżeń”. Clang wskazane, ale nie wymagane.
deworde
24

tl; dr Kod generuje test, że liczba całkowita + liczba całkowita dodatnia == liczba całkowita ujemna . Zwykle optymalizator nie optymalizuje tego na zewnątrz, ale w konkretnym przypadku std::endlużycia następnego kompilator optymalizuje ten test. Jeszcze nie wymyśliłem, co jest takiego specjalnego endl.


Z kodu asemblera na -O1 i wyższych poziomach jasno wynika, że ​​gcc refaktoruje pętlę do:

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

Największa wartość, która działa poprawnie, to 715827882np. Floor ( INT_MAX/3). Fragment kodu zespołu pod adresem -O1to:

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

Uwaga, -1431655768jest 4 * 715827882w uzupełnieniu do dwójki.

Trafienie -O2optymalizuje to do następujących:

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

Tak więc optymalizacja, która została dokonana, polega po prostu na tym, że addlzostał przeniesiony wyżej.

Jeśli ponownie skompilujemy z 715827883 zamiast tego wersja -O1 będzie identyczna poza zmienioną liczbą i wartością testową. Jednak -O2 powoduje zmianę:

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

Tam, gdzie było cmpl $-1431655764, %esiw -O1, ta linia została usunięta dla -O2. Optymalizator musiał zdecydować, że dodanie 715827883do%esi nigdy nie może się równać-1431655764 .

To dość zagadkowe. Dodając, że aby INT_MIN+1 nie generować oczekiwanego rezultatu, więc optymalizator musi zdecydowały, że %esinigdy nie może byćINT_MIN+1 i nie jestem pewien, dlaczego to zdecydować, że.

W przykładzie roboczym wydaje się równie słuszne stwierdzenie, że dodawanie 715827882do liczby nie może się równać INT_MIN + 715827882 - 2! (jest to możliwe tylko wtedy, gdy faktycznie występuje zawijanie), ale nie optymalizuje to linii w tym przykładzie.


Kod, którego użyłem, to:

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

Jeśli std::endl(std::cout)zostanie usunięty, optymalizacja nie będzie już występować. W rzeczywistości zastąpienie go std::cout.put('\n'); std::flush(std::cout);również powoduje, że optymalizacja nie występuje, mimo że std::endljest wbudowana.

std::endlWydaje się, że inlining of wpływa na wcześniejszą część struktury pętli (co nie do końca rozumiem, co robi, ale opublikuję to tutaj, na wypadek gdyby zrobił to ktoś inny):

Z oryginalnym kodem i -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

Z mymanual inline z 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

Jedna różnica między tymi dwoma polega na tym, że %esijest używana w oryginale i %ebxw drugiej wersji; czy jest jakaś różnica w semantyce zdefiniowanej między %esii %ebxogólnie? (Nie wiem zbyt wiele o montażu x86).

MM
źródło
Dobrze byłoby jednak dowiedzieć się dokładnie, jaka była logika optymalizatora, na tym etapie nie jest dla mnie jasne, dlaczego w niektórych przypadkach test został zoptymalizowany, a niektórzy nie
MM
8

Innym przykładem tego błędu zgłaszanego w gcc jest pętla, która jest wykonywana dla stałej liczby iteracji, ale używasz zmiennej licznika jako indeksu w tablicy, która ma mniejszą liczbę elementów, na przykład:

int a[50], x;

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

Kompilator może określić, że ta pętla będzie próbowała uzyskać dostęp do pamięci poza tablicą „a”. Kompilator narzeka na to z tym dość tajemniczym komunikatem:

iteracja xxu wywołuje niezdefiniowane zachowanie [-Werror = agresywne-optymalizacje-pętli]

Ed Tyler
źródło
Jeszcze bardziej tajemnicze jest to, że wiadomość jest emitowana tylko wtedy, gdy optymalizacja jest włączona. Komunikat M $ VB „Tablica poza granicami” jest przeznaczona dla opornych?
Ravi Ganesh
6

Czego nie mogę uzyskać, to dlaczego wartość jest zepsuta przez tę operację przepełnienia?

Wydaje się, że przepełnienie całkowitoliczbowe występuje w 4. iteracji (for i = 3). signedprzepełnienie całkowitoliczbowe wywołuje niezdefiniowane zachowanie . W tym przypadku nic nie można przewidzieć. Pętla może powtarzać się tylko 4razy, może ciągnąć się w nieskończoność lub cokolwiek innego!
Wynik może różnić się kompilatorem do kompilatora lub nawet dla różnych wersji tego samego kompilatora.

C11: 1.3.24 niezdefiniowane zachowanie:

zachowanie, dla którego niniejsza norma międzynarodowa nie nakłada żadnych wymagań
[Uwaga: można oczekiwać nieokreślonego zachowania, gdy w niniejszej normie międzynarodowej pomija się jakąkolwiek wyraźną definicję zachowania lub gdy program używa błędnej konstrukcji lub błędnych danych. Dopuszczalne niezdefiniowane zachowanie waha się od całkowitego zignorowania sytuacji z nieprzewidywalnymi skutkami, poprzez zachowanie podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z lub bez wydania komunikatu diagnostycznego), aż po przerwanie tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego) . Wiele błędnych konstrukcji programów nie wywołuje nieokreślonego zachowania; wymagana jest diagnoza. —End note]

haccks
źródło
@bits_international; Tak.
haccks
4
Masz rację, uczciwie jest wyjaśnić, dlaczego przegłosowałem. Informacje w tej odpowiedzi są poprawne, ale nie mają charakteru edukacyjnego i całkowicie ignorują słonia w pokoju: pęknięcie najwyraźniej następuje w innym miejscu (stan zatrzymania) niż operacja powodująca przepełnienie. Mechanika tego, jak rzeczy się psują w tym konkretnym przypadku, nie została wyjaśniona, mimo że jest to sedno tego pytania. Jest to typowa zła sytuacja nauczyciela, w której odpowiedź nauczyciela nie tylko nie dotyczy sedna problemu, ale zniechęca do dalszych pytań. Brzmi prawie jak ...
Szabolcs
5
„Widzę, że jest to niezdefiniowane zachowanie i od tego momentu nie obchodzi mnie, jak i dlaczego się psuje. Norma pozwala na to. Żadnych dalszych pytań”. Może nie miałeś tego na myśli, ale na to wygląda. Mam nadzieję, że mniej tego (niestety powszechnego) stosunku do SO będzie mniej. Nie jest to praktycznie przydatne. Jeśli otrzymujesz dane wejściowe użytkownika, nie jest rozsądne sprawdzanie przepełnienia po każdej operacji na liczbach całkowitych ze znakiem , nawet jeśli standard mówi, że jakakolwiek inna część programu może z tego powodu wybuchnąć. Zrozumienie, jak się psuje , pomaga w praktyce uniknąć takich problemów.
Szabolcs
2
@Szabolcs: Najlepiej byłoby myśleć o C jako o dwóch językach, z których jeden został zaprojektowany, aby umożliwić prostym kompilatorom osiągnięcie rozsądnie wydajnego kodu wykonywalnego z pomocą programistów, którzy wykorzystują konstrukcje, które byłyby niezawodne na zamierzonych platformach docelowych, ale nie inne i zostały w konsekwencji zignorowane przez komitet ds. standardów, oraz drugi język, który wyklucza wszystkie takie konstrukcje, dla których norma nie wymaga wsparcia, w celu umożliwienia kompilatorom zastosowania dodatkowych optymalizacji, które mogą, ale nie muszą, przeważać nad tymi, które programiści muszą poddać się.
supercat
1
@Szabolcs „ Jeśli otrzymujesz dane wejściowe użytkownika, nie jest rozsądne sprawdzanie przepełnienia po każdej operacji na liczbach całkowitych ze znakiem” - poprawne, ponieważ w tym momencie jest już za późno. Musisz sprawdzić przepełnienie przed każdą operacją na liczbach całkowitych ze znakiem.
melpomene