GCC skraca x/x do 1 nawet przy zerze: analiza UB i stałej redukcji
Programista spodziewał się, że program w C++ z wejściem x i wyjściem x / x ugrzęźnie przy x = 0, pokazując błąd dzielenia przez zero. Zamiast tego GCC wypisał 1 bez żadnych wyjątków. Przyczyną jest stała redukcja: kompilator przekształcił x / x w stałą wartość 1, ignorując rzeczywistą wartość x w czasie działania.
Kod do demonstracji:
#include <iostream>
int main()
{
int x;
std::cin >> x;
std::cout << x / x << std::endl;
}
Przy x = 0 zamiast SIGFPE lub wyjątku — ciche wypisanie 1. Flagi takie jak -O0 nie zmieniają zachowania: redukcja działa już na poziomie podstawowym.
Mechanizm stałej redukcji w GCC
Kompilator stosuje tożsamość algebraiczną: x / x = 1 dla x ≠ 0. W kodzie maszynowym nie ma dzielenia — tylko mov wartości stałe 1. Jest to standardowa optymalizacja, ale bez sprawdzania warunku.
GCC rozumuje następująco: dzielenie przez zero to UB (undefined behavior) zgodnie ze standardem C++. Standard nie określa zachowania, więc kompilator zakłada, że x ≠ 0, i dokonuje redukcji wyrażenia.
Porównanie kompilatorów:
- GCC/Clang: redukcja do
1, brak dzielenia. - MSVC: generuje instrukcję dzielenia, rzuca wyjątkiem przy
x = 0.
Oba podejścia są zgodne ze standardem — UB daje kompilatorom swobodę działania.
Podstawa matematyczna: błąd skrócenia
Skracanie x w x / x jest dopuszczalne tylko gdy x ≠ 0. Kompilator narusza to regułę, podobnie jak szkolny błąd w "udowodnieniu" 1=2:
Powiedzmy, że a = b
Wtedy a² = ab
...
(a-b)(a+b) = b(a-b) ← dzielenie przez (a-b)=0 ukryte
...
2 = 1
Błąd polega na skróceniu bez sprawdzenia mianownika. GCC powtarza ten błąd: zakłada x ≠ 0 na podstawie UB.
Możliwości UB dla kompilatora
Przy UB kompilator może:
- Wygenerować dzielenie (wyjątek sprzętowy).
- Usunąć kod jako niedostępną część.
- Wstawić stałą.
- Zignorować gałąź.
Zasada: "UB nie występuje". Kompilator optymalizuje zakładając brak UB, co zwiększa wydajność, ale zmniejsza przewidywalność.
Jak zmusić GCC do awarii
Użyj volatile, aby zabronić założeń:
#include <iostream>
int main()
{
int tmp;
std::cin >> tmp;
volatile int x = tmp;
std::cout << x / x << std::endl;
}
volatile blokuje redukcję: kompilator generuje rzeczywiste dzielenie, a x=0 powoduje SIGFPE.
Co ważne
- Stała redukcja w GCC stosuje
x/x=1nawet przyx=0w czasie działania dzięki UB. volatilezapobiega optymalizacji, zapewniając oczekiwaną awarię.- Standard C++ daje kompilatorom pełną swobodę przy UB, MSVC jest bardziej matematycznie uczciwy.
- Sprawdzaj kod przed użyciem w naukach: optymalizacje ukrywają UB.
- UB to nie błąd — to wolność dla agresywnych transformacji.
Konsekwencje dla deweloperów
To zachowanie podkreśla ryzyko UB: kompilator nie sygnalizuje błędów, a cicho zmienia semantykę. Dla deweloperów średnio- i wysokiego poziomu lekcja brzmi: ostrożność przy wyrażeniach, gdzie możliwa jest wartość zero:
- Unikaj
x/xw kodzie ogólnym. - Testuj z przypadkami granicznymi na różnych kompilatorach.
- Używaj
volatilelub sprawdzeńif (x != 0)dla przewidywalności.
Standard balansuje wydajność i bezpieczeństwo poprzez UB. Dyskusja: czy warto upośledzić zasady dla dzielenia przez zero, uczynić je zdefiniowanym (np. trap)?
— Editorial Team
Brak komentarzy.