Poniższy kod przechodzi w nieskończoną pętlę w GCC:
#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;
}
Oto umowa: Przepełnienie liczby całkowitej ze znakiem jest technicznie niezdefiniowanym zachowaniem. Ale GCC na x86 implementuje arytmetykę liczb całkowitych przy użyciu instrukcji całkowitych x86 - które zawijają się po przepełnieniu.
Dlatego spodziewałbym się, że zawinie się na przepełnieniu - pomimo tego, że jest to niezdefiniowane zachowanie. Ale tak nie jest. Zatem ... co mnie ominęło?
Skompilowałem to używając:
~/Desktop$ g++ main.cpp -O2
Wyjście GCC:
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
0
0
0
... (infinite loop)
Przy wyłączonych optymalizacjach nie ma nieskończonej pętli, a wyjście jest poprawne. Visual Studio również poprawnie to kompiluje i daje następujący wynik:
Prawidłowe wyjście:
~/Desktop$ g++ main.cpp
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
3
Oto kilka innych odmian:
i *= 2; // Also fails and goes into infinite loop.
i <<= 1; // This seems okay. It does not enter infinite loop.
Oto wszystkie istotne informacje o wersji:
~/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$
Więc pytanie brzmi: czy to błąd w GCC? A może źle zrozumiałem coś o tym, jak GCC obsługuje arytmetykę liczb całkowitych?
* Oznaczam również to C, ponieważ zakładam, że ten błąd odtworzy się w C. (jeszcze go nie zweryfikowałem).
EDYTOWAĆ:
Oto montaż pętli: (jeśli dobrze ją rozpoznałem)
.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
.Odpowiedzi:
Kiedy norma mówi, że jest to niezdefiniowane zachowanie, oznacza to . Wszystko może się zdarzyć. „Wszystko” obejmuje „zwykle zawijają się liczby całkowite, ale czasami zdarzają się dziwne rzeczy”.
Tak, na procesorach x86 liczby całkowite zwykle zawijają się tak, jak się spodziewasz. To jeden z tych wyjątków. Kompilator zakłada, że nie spowoduje to niezdefiniowanego zachowania i optymalizuje test pętli. Jeśli naprawdę chcesz zawinąć, przekaż
-fwrapv
dog++
lubgcc
podczas kompilacji; daje to dobrze zdefiniowaną (uzupełnienie dwóm) semantykę przepełnienia, ale może zaszkodzić wydajności.źródło
-fwrapv
. Dzięki za zwrócenie uwagi.To proste: niezdefiniowane zachowanie - zwłaszcza przy
-O2
włączonej optymalizacji ( ) - oznacza, że wszystko może się zdarzyć.Twój kod zachowuje się zgodnie z oczekiwaniami bez
-O2
przełącznika.Nawiasem mówiąc, działa całkiem dobrze z icl i tcc, ale nie możesz polegać na takich rzeczach ...
Zgodnie z tym optymalizacja gcc w rzeczywistości wykorzystuje przepełnienie liczb całkowitych ze znakiem. Oznaczałoby to, że „błąd” jest z założenia.
źródło
for (j = i; j < i + 10; ++j) ++k;
, po prostu ustawi sięk = 10
, ponieważ zawsze będzie to prawda, jeśli nie wystąpi przepełnienie ze znakiem.Ważną rzeczą do zapamiętania jest to, że programy w C ++ są pisane dla maszyny abstrakcyjnej C ++ (która jest zwykle emulowana za pomocą instrukcji sprzętowych). Fakt, że kompilujesz dla x86, jest całkowicie bez znaczenia dla faktu, że ma to niezdefiniowane zachowanie.
Kompilator może wykorzystać istnienie niezdefiniowanego zachowania, aby ulepszyć swoje optymalizacje (poprzez usunięcie warunku z pętli, jak w tym przykładzie). Nie ma żadnego gwarantowanego, ani nawet użytecznego, mapowania między konstrukcjami poziomu C ++ a konstrukcjami kodu maszynowego na poziomie x86 poza wymogiem, że kod maszynowy po wykonaniu wygeneruje wynik wymagany przez maszynę abstrakcyjną C ++.
źródło
i += i;
// przepełnienie jest niezdefiniowane.
Z -fwrapv jest to poprawne. -fwrapv
źródło
Proszę, ludzie, niezdefiniowane zachowanie jest dokładnie tym, niezdefiniowane . To znaczy, że wszystko może się zdarzyć. W praktyce (jak w tym przypadku) kompilator może założyć, że tak się nie staniebyć wezwanym i rób, co tylko zechcesz, jeśli dzięki temu kod będzie szybszy / mniejszy. Nikt nie zgaduje, co dzieje się z kodem, który nie powinien działać. Będzie to zależeć od otaczającego kodu (w zależności od tego kompilator może wygenerować inny kod), użytych zmiennych / stałych, flag kompilatora, ... Aha, a kompilator może zostać zaktualizowany i napisać ten sam kod inaczej, lub możesz pobierz inny kompilator z innym widokiem na generowanie kodu. Lub po prostu zdobądź inną maszynę, nawet inny model w tej samej linii architektury może mieć swoje własne niezdefiniowane zachowanie (sprawdź niezdefiniowane kody operacyjne, niektórzy przedsiębiorczy programiści odkryli, że na niektórych z tych wczesnych maszyn czasami robili użyteczne rzeczy ...) . Jest ma„kompilator zapewnia określone zachowanie przy niezdefiniowanym zachowaniu”. Są obszary, które są zdefiniowane w ramach implementacji i tam powinieneś być w stanie liczyć na konsekwentne zachowanie kompilatora.
źródło
Nawet jeśli kompilator miałby określić, że przepełnienie całkowitoliczbowe należy uznać za „niekrytyczną” formę niezdefiniowanego zachowania (zgodnie z definicją w załączniku L), wynik przepełnienia liczb całkowitych powinien, w przypadku braku określonej platformy, obiecać bardziej specyficzne zachowanie co najmniej traktowane jako „wartość częściowo nieokreślona”. Zgodnie z takimi zasadami dodanie 1073741824 + 1073741824 można arbitralnie uznać za dające 2147483648 lub -2147483648 lub jakąkolwiek inną wartość, która była zgodna z 2147483648 mod 4294967296, a wartości uzyskane w wyniku dodawania można arbitralnie uznać za dowolną wartość zgodną z 0 mod 4294967296.
Reguły zezwalające na przepełnienie w celu uzyskania „częściowo nieokreślonych wartości” byłyby wystarczająco dobrze zdefiniowane, aby były zgodne z literą i duchem załącznika L, ale nie uniemożliwiałyby kompilatorowi wyciągania tych samych ogólnie użytecznych wniosków, które byłyby uzasadnione, gdyby przepełnienia nie były ograniczone Niezdefiniowane zachowanie. Uniemożliwiłoby to kompilatorowi dokonywanie fałszywych „optymalizacji”, których głównym efektem w wielu przypadkach jest wymaganie od programistów dodania dodatkowego bałaganu do kodu, którego jedynym celem jest zapobieganie takim „optymalizacjom”; czy to byłoby dobre, czy nie, zależy od punktu widzenia.
źródło