Zpět na domů

GCC skládá x/x do 1 při dělení nulou

Článek rozebírá případ, kdy GCC skládá x/x do konstanty 1 i při x=0 kvůli UB. Popsán mechanismus skládání konstant, porovnání s MSVC, řešení s volatile a důsledky pro vývoj.

x/x = 1 v GCC: léčka skládání konstant a UB
Advertisement 728x90

GCC zjednodušil x/x na 1 i při nule: analýza chování a konstantního zjednodušení

Vývojář očekával, že program v C++ s vstupem x a výstupem x / x spadne při x = 0, což by ukázalo chybu dělení nulou. Namísto toho vydal GCC hodnotu 1 bez výjimky. Toto se stalo kvůli konstantnímu zjednodušení: kompilátor převedl x / x na konstantu 1, ignoroval runtime hodnotu x.

Kód pro demonstraci:

#include <iostream>
int main()
{
    int x;
    std::cin >> x;
    std::cout << x / x << std::endl;
}

Při x = 0 místo SIGFPE nebo výjimky – tichý výstup 1. Přepínač -O0 nezmění chování: zjednodušení funguje již na úrovni základních pravidel.

Google AdInline article slot

Mechanismus konstantního zjednodušení v GCC

Kompilátor používá algebraické identitu: x / x = 1 pro x ≠ 0. V strojovém kódu není žádné dělení – jen mov konstanty 1. Jedná se o standardní optimalizaci, ale bez ověření podmínky.

GCC uvažuje takto: dělení nulou je chování nedefinované (UB) podle standardu C++. Standard neurčuje chování, proto kompilátor předpokládá x ≠ 0 a zjednoduší výraz.

Srovnání kompilátorů:

Google AdInline article slot
  • GCC/Clang: zjednoduší na 1, žádné dělení.
  • MSVC: generuje instrukci dělení, vyhazuje výjimku při x = 0.

Oba přístupy odpovídají standardu – UB kompilátorům dává svobodu.

Matematická podstata: chyba zjednodušení

Zjednodušení x ve výrazu x / x je dovoleno pouze pro x ≠ 0. Kompilátor tento požadavek porušuje, stejně jako školní chyba v "důkazu" 1=2:

Předpokládejme a = b
Potom a² = ab
...
(a-b)(a+b) = b(a-b)  ← dělení (a-b)=0 je skryté
...
2 = 1

Chyba je v kroku zjednodušení bez ověření jmenovatele. GCC opakuje tento postup: předpokládá x ≠ 0 kvůli UB.

Google AdInline article slot

Možnosti chování nedefinovaného chování pro kompilátor

Při UB může kompilátor:

  • Vygenerovat dělení (hardwarová výjimka).
  • Odstranit kód jako nedosažitelný.
  • Nahradit konstantou.
  • Ignorovat větev.

Princip: "UB se nestane". Kompilátor optimalizuje pod předpokladem, že UB nemůže nastat, což zvyšuje výkon, ale snižuje předvídatelnost.

Jak donutit GCC ke spadnutí

Použijte volatile, aby byly zabráněny předpoklady:

#include <iostream>
int main()
{
    int tmp;
    std::cin >> tmp;
    volatile int x = tmp;
    std::cout << x / x << std::endl;
}

volatile blokuje zjednodušení: kompilátor generuje skutečné dělení, x=0 způsobí SIGFPE.

Co je důležité

  • Konstantní zjednodušení v GCC aplikuje x/x=1 i při runtime x=0 kvůli UB.
  • volatile zabrání optimalizaci a zajistí očekávané selhání.
  • Standard C++ kompilátorům dává volnost při UB, MSVC je matematicky čestnější.
  • Zkontrolujte kód před použitím v výuce: optimalizace maskují UB.
  • UB není "chyba", ale prostor pro agresivní transformace.

Důsledky pro vývoj

Toto chování zdůrazňuje rizika UB: kompilátor neupozorní na chyby, ale tichounce mění smysl. Pro střední a senior vývojáře je lekcí opatrnost při výrazech, kde může být nula:

  • Vyhněte se x/x v obecném kódu.
  • Testujte s okrajovými případy na různých kompilátorech.
  • Používejte volatile nebo kontrolu if (x != 0) pro předvídatelnost.

Standard vyváží výkon a bezpečnost za cenu UB. Diskuze: Stojí za to ztvrdit pravidla pro dělení nulou a udělat jej definované (např. trap)?

— Editorial Team

Advertisement 728x90

Číst dál