Что такое правило строгого алиасинга и как оно может вас укусить?
Эта процедура битовых трюков проходит на -O0, но возвращает неверный результат при включённой оптимизации. Объясните почему и перепишите корректно.
#include <cstdint>
float fast_inverse_sqrt_bad(float x) {
std::int32_t i = *reinterpret_cast<std::int32_t*>(&x);
i = 0x5f3759df - (i >> 1);
return *reinterpret_cast<float*>(&i);
}
Найдите и исправьте ошибку.
Правило строгого алиасинга гласит, что к хранимому значению можно обращаться только через lvalue его собственного типа, совместимого типа или char. Чтение float через int* — неопределённое поведение, поэтому оптимизатор считает, что такие указатели не пересекаются, и может переупорядочить или выбросить чтения. Безопасные инструменты — std::memcpy и std::bit_cast.
- ✗Делать
reinterpret_castмежду несвязанными типами указателей и разыменовывать — классическое UB - ✗Считать поведение на
-O0доказательством корректности — алиасинг ломается лишь при оптимизации - ✗Полагать, что punning через
unionпереносим в C++ — он определён в C, но UB в C++
- →Почему
std::bit_castстрого лучше punning черезmemcpyдля тривиальных типов? - →Как
char*получает особое исключение из правила алиасинга?
Что не так с этим кодом?
#include <cstring>
#include <cstdint>
#include <bit>
// НЕВЕРНО: type punning через reinterpret_cast — UB
float fast_inverse_sqrt_bad(float x) {
std::int32_t i = *reinterpret_cast<std::int32_t*>(&x); // нарушение алиасинга
i = 0x5f3759df - (i >> 1);
return *reinterpret_cast<float*>(&i); // снова нарушение
}
// ВЕРНО (C++11+): memcpy не имеет ограничений алиасинга
float bits_to_float_memcpy(std::int32_t i) {
float f;
std::memcpy(&f, &i, sizeof f); // байтовая копия, поведение определено
return f;
}
// ВЕРНО (C++20): bit_cast — выразительно и constexpr-пригодно
float bits_to_float_bitcast(std::int32_t i) {
return std::bit_cast<float>(i);
}
fast_inverse_sqrt_bad обращается к хранимому float через lvalue типа std::int32_t. Эти типы не связаны для целей алиасинга, поэтому каждое разыменование — неопределённое поведение. На -O0 оно «работает», но с включённой оптимизацией компилятор вправе считать, что int32_t* и float* не алиасят: он может переупорядочить запись в i относительно чтения через &x, кэшировать устаревшее значение или вовсе выбросить операцию. Результат — тихие, зависящие от уровня оптимизации баги.
Разрешённые типы для доступа: собственный тип объекта, его cv/знаково-совместимый вариант и char/unsigned char/std::byte. Для переинтерпретации представления используйте std::memcpy (с C++11) или std::bit_cast (с C++20) — обе версии корректны, а bit_cast к тому же годится в constexpr.