Kompilowanie tego:
#include <iostream>
int main()
{
for (int i = 0; i < 4; ++i)
std::cout << i*1000000000 << std::endl;
}
i gcc
generuje 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 i
ta 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)
c++
gcc
undefined-behavior
zerkms
źródło
źródło
O2
iO3
, ale nieO0
lubO1
Odpowiedzi:
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
Twój kod skompilowany z
g++ -O3
emituje ostrzeżenie (nawet bez-Wall
)Jedynym sposobem, w jaki możemy przeanalizować, co robi program, jest odczytanie wygenerowanego kodu asemblera.
Oto pełna lista montażu:
Ledwo mogę czytać montaż, ale nawet ja widzę
addl $1000000000, %edi
linię. Wynikowy kod wygląda bardziej jakTen komentarz @TC:
dał mi pomysł porównania kodu assemblera kodu OP z kodem assemblera następującego kodu, bez niezdefiniowanego zachowania.
W rzeczywistości poprawny kod ma warunek zakończenia.
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
-Wall
to opcja gcc, która włącza wszystkie przydatne ostrzeżenia bez fałszywych alarmów. To absolutne minimum, którego zawsze powinieneś używać.-Wall
ponieważ mogą ostrzegać przed fałszywymi alarmamiużyj flag debugowania do debugowania
-ftrapv
zatrzymuje program na przepełnieniu,-fcatch-undefined-behavior
łapie dużo przypadków zachowanie niezdefiniowane (uwaga:"a lot of" != "all of them"
)Użyj gcc
-fwrapv
1 - ta zasada nie ma zastosowania do „przepełnienia liczby całkowitej bez znaku”, zgodnie z §3.9.1.4
i np. wynik
UINT_MAX + 1
jest określony matematycznie - przez reguły arytmetyczne modulo 2 nźródło
i
dotyczy? Ogólnie niezdefiniowane zachowanie nie ma takich dziwnych skutków ubocznych, w końcui*100000000
powinno być rvaluei
o dowolnej wartości większej niż 2 ma niezdefiniowane zachowanie -> (2) możemy założyć, żei <= 2
dla celów optymalizacyjnych -> (3) warunek pętli jest zawsze prawdziwy -> (4 ) jest zoptymalizowany w nieskończoną pętlę.i
o 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”.Krótka odpowiedź,
gcc
konkretnie udokumentowała ten problem, widzimy, że w informacjach o wydaniu gcc 4.8, które mówi ( podkreślenie moje w przyszłości ):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:Wiemy, że norma mówi, że niezdefiniowane zachowanie jest nieprzewidywalne na podstawie notatki dołączonej do definicji, która mówi:
Ale co na świecie może zrobić
gcc
optymalizator, aby przekształcić to w nieskończoną pętlę? Brzmi kompletnie głupio. Ale na szczęściegcc
daje nam wskazówkę, jak to rozgryźć w ostrzeżeniu: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:artykuł mówi:
a później mówi:
Kompilator musi więc w niektórych przypadkach założyć, że przepełnienie ze znakiem całkowitym jest niezdefiniowanym zachowaniem, to
i
zawsze musi być mniejsze,4
a 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:
gcc
wywnioskowałem, że ponieważs
został odrzucony w,s->f;
a ponieważ dereferencja pustego wskaźnika jest niezdefiniowanym zachowaniem,s
nie może być zerowa i dlatego optymalizujeif (!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.
źródło
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::endl
użycia następnego kompilator optymalizuje ten test. Jeszcze nie wymyśliłem, co jest takiego specjalnegoendl
.Z kodu asemblera na -O1 i wyższych poziomach jasno wynika, że gcc refaktoruje pętlę do:
Największa wartość, która działa poprawnie, to
715827882
np. Floor (INT_MAX/3
). Fragment kodu zespołu pod adresem-O1
to:Uwaga,
-1431655768
jest4 * 715827882
w uzupełnieniu do dwójki.Trafienie
-O2
optymalizuje to do następujących:Tak więc optymalizacja, która została dokonana, polega po prostu na tym, że
addl
został 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ę:Tam, gdzie było
cmpl $-1431655764, %esi
w-O1
, ta linia została usunięta dla-O2
. Optymalizator musiał zdecydować, że dodanie715827883
do%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%esi
nigdy 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
715827882
do 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:
Jeśli
std::endl(std::cout)
zostanie usunięty, optymalizacja nie będzie już występować. W rzeczywistości zastąpienie gostd::cout.put('\n'); std::flush(std::cout);
również powoduje, że optymalizacja nie występuje, mimo żestd::endl
jest wbudowana.std::endl
Wydaje 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
:Z mymanual inline z
std::endl
,-O2
:Jedna różnica między tymi dwoma polega na tym, że
%esi
jest używana w oryginale i%ebx
w drugiej wersji; czy jest jakaś różnica w semantyce zdefiniowanej między%esi
i%ebx
ogólnie? (Nie wiem zbyt wiele o montażu x86).źródło
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:
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:
źródło
Wydaje się, że przepełnienie całkowitoliczbowe występuje w 4. iteracji (for
i = 3
).signed
przepełnienie całkowitoliczbowe wywołuje niezdefiniowane zachowanie . W tym przypadku nic nie można przewidzieć. Pętla może powtarzać się tylko4
razy, 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:
źródło