Знаковые целочисленные типы, переполнение и оптимизация
Арифметика беззнаковых (unsigned) целых чисел в C++ работает по модулю степени двойки (пруф). То есть, например, значение 255 + 1
для 8-битного типа определено и равно 0. Однако на знаковые (signed) типы такие гарантии не распространяются, и переполнение является undefined behavior. Это даёт возможность компилятору предположить, что переполнение для знаковых типов не наступает никогда, из-за чего становятся возможными некоторые неочевидные оптимизации.
Пример 1
С точки зрения арифметики, a < a+1
для любого a
. Поэтому компилятор имеет право предположить, что первое условие в примере ниже выполняется всегда, и убрать проверку. Однако если честно выполнить сложение, то то же самое число a + 1
окажется меньше a
. [запустить]
int a = std::numeric_limits<int>::max();
std::cout << (a < a + 1) << "\n";
// -> true
int b = a + 1;
std::cout << (a < b) << "\n";
// -> false
Аналогично, 128 + 1 ≠ -127
: [запустить]
using schar = signed char;
constexpr int sz = sizeof(schar);
schar a = std::numeric_limits<schar>::max();
std::cout << (a + 1 == std::numeric_limits<schar>::min()) << "\n";
// -> false
schar b = a + 1;
schar c = std::numeric_limits<schar>::min();
std::cout << (std::memcmp(&b, &c, sz) == 0);
// -> true
Пример 2
Для любых a ∈ ℤ⁺ и b = 2ⁿ верно, что a % b == a & (b - 1)
. Компилятор пользуется этим: [запустить]
using schar = signed char;
constexpr int sz = sizeof(schar);
schar m = std::numeric_limits<schar>::max();
schar a = m + 128;
std::cout << std::bitset<sz*8>((m + 128) % 2) << "\n";
// -> 00000001, which is 1
std::cout << std::bitset<sz*8>(a % 2) << "\n";
// -> 11111111, which is -1
Заключение
В приведённых примерах поведение кода зависит от того, как компилятор воспользуется undefined behavior. Будьте внимательнее и не допускайте UB, иначе вас будет ждать сюрприз.
Статья была вдохновлена Krister Walfridsson’s “How undefined signed overflow enables optimizations in GCC”.
Также про UB может быть интересно прочесть Krister Walfridsson’s “Why undefined behavior may call a never-called function” или перевод на хабре.
Хорошая серия постов про UB от разработчика LLVM: Chris Lattner, “What Every C Programmer Should Know About Undefined Behavior”.