Каковы последствия математики с плавающей запятой constexpr?
Начиная с C++11, мы можем выполнять математические операции с плавающей запятой во время компиляции. В C++23 и C++26 добавлены некоторые функции, но не все.
Математика с плавающей запятой вообще странная, потому что результаты не совсем точны. Однако предполагается, что код всегда обеспечивает согласованные результаты. Как C++ подходит к этой проблеме?
Вопросы
- Как работает математика с плавающей запятой?
- Одинаковы ли результаты для всех компиляторов?
- Одинаковы ли результаты во время компиляции и во время выполнения одного и того же компилятора?
- Почему некоторые функции
constexpr
, а другие нет (например,std::nearbyint
)
2 ответа
C++ накладывает очень мало ограничений на поведениеfloat
и другие типы с плавающей запятой. Это может привести к возможным несоответствиям результатов как между компиляторами, так и между оценками во время выполнения/компиляции одним и тем же компилятором. Вот информация об этом:
Ошибки с плавающей запятой
Некоторые операции могут завершиться неудачей, например деление на ноль. Стандарт C++ гласит:
Если второй операнд / или % равен нулю, поведение не определено.
В константных выражениях это соблюдается, поэтому невозможно получить NaN с помощью операций или повышенияFE_DIVBYZERO
во время компиляции.
Для чисел с плавающей запятой не делается никаких исключений. Однако, когдаstd::numeric_limits<float>::is_iec559()
являетсяtrue
, большинство компиляторов будут иметь соответствие IEEE-754 в качестве расширения. Например, допускается деление на ноль, которое дает бесконечность или NaN в зависимости от операндов.
Режимы округления
C++ всегда допускал различия между результатами времени компиляции и результатами времени выполнения. Например, вы можете оценить:
double x = 10.0f / 3.0;
constexpr double y = 10.0 / 3.0;
assert(x == y); // might fail
Результат может не всегда быть одинаковым, поскольку среду с плавающей запятой можно изменить только во время выполнения, и, следовательно, можно изменить режим округления.
Подход C++ заключается в том, чтобы эффект среды с плавающей запятой определялся реализацией. Он не дает вам переносимого способа управления им (и, следовательно, округлением) в константных выражениях.
Если [
FENVC_ACCESS
] используется для включения управления средой с плавающей запятой, в этом документе не указано влияние на вычисление с плавающей запятой в константных выражениях.
Оптимизация компилятора
Во-первых, компиляторы могут захотеть оптимизировать ваш код, даже если это меняет его смысл. Например, GCC оптимизирует этот вызов:
// No call to sqrt thanks to constant folding.
// This ignores the fact that this is a runtime evaluation, and would normally be impacted
// by the floating point environment at runtime.
const float x = std::sqrt(2);
Семантика меняется еще больше с помощью таких флагов, которые позволяют компилятору переупорядочивать и оптимизировать операции способом, не совместимым с IEEE-754. Например:
float big() { return 1e20f;}
int main() {
std::cout << big() + 3.14f - big();
}
Для чисел с плавающей запятой IEEE-754 сложение и вычитание не являются коммутативными. Мы не можем оптимизировать это, чтобы:(big() - big()) + 3.14f
. Результат будет0
, потому что слишком мал, чтобы вносить какие-либо измененияbig()
при добавлении из-за отсутствия точности. Однако с-ffast-math
включен, результат может быть3.14f
.
Математические функции
Могут быть различия во время выполнения константных выражений для всех операций, включая вызовы математических функций. во время компиляции может быть не таким, какstd::sqrt(2)
во время выполнения. Однако эта проблема характерна не только для математических функций. Вы можете разделить эти функции на следующие категории:
Нет зависимости от FPENV/очень слабая зависимость (начиная с C++23) [P05333r9]
Некоторые функции полностью независимы от среды с плавающей запятой или просто не могут выйти из строя, например:
-
std::ceil
(округлить до следующего большего числа) -
std::fmax
(максимум два числа) -
std::signbit
(получает знаковый бит числа с плавающей запятой)
Кроме того, есть такие функции, какstd::fma
которые просто объединяют две операции с плавающей запятой. Это не более проблематично, чем и во время компиляции. Поведение такое же, как при вызове этих математических функций в C (см. Стандарт C23, Приложение F.8.4 ), однако это не константное выражение в C++, если есть исключения, отличные отFE_INEXACT
подняты,errno
установлен и т. д. (см. [library.c]/3).
Слабая зависимость FPENV (начиная с C++26)
Другие функции зависят от среды с плавающей запятой, например:std::sqrt
илиstd::sin
. Однако эта зависимость называется слабой , потому что она не указана явно и существует только потому, что математика с плавающей запятой по своей сути неточна.
Было бы произвольно допускать+
и*
во время компиляции, но не математические функции, которые имеют те же проблемы.
Математические специальные функции (пока нет, возможно, в будущем)
[P1383r0][P1383r0] посчитал слишком амбициозным добавление математических специальных функций , таких как:
-
std::beta
-
std::riemann_zeta
- и многое другое...
Сильная зависимость от FPENV (пока нет, возможно, никогда)
Некоторые функции, такие как явно указано, используют текущий режим округления в стандарте. Это проблематично, поскольку вы не можете управлять средой с плавающей запятой во время компиляции стандартными средствами. Функций типа нет и, возможно, никогда не будет.
Заключение
Подводя итог, можно сказать, что существует множество проблем, с которыми сталкиваются комитеты по стандартизации и разработчики компиляторов при работе с математикой. Потребовались десятилетия дискуссий, чтобы снять некоторые ограничения наconstexpr
математические функции, но мы наконец здесь. Ограничения варьировались от произвольных в случаеstd::fabs
, необходимо в случаеstd::nearbyint
.
Вероятно, в будущем мы увидим дальнейшие ограничения, по крайней мере, для специальных математических функций .
Ян Шультке уже дал отличный ответ, я просто хочу устранить некоторые потенциальные недоразумения:
Время компиляции не то же самое, что
Начиная с C++11, мы можем выполнять математические операции с плавающей запятой во время компиляции.
Это не правда. Компиляторы уже давно могут выполнять математические вычисления во время компиляции, и ничто в старых версиях C++ этому не мешало. GCC и Clang с радостью выполняют деление с плавающей запятой во время компиляции безconstexpr
, даже с-std=c++98 -O0
.
Кроме того, полезно иметь в виду, что единственное требование дляconstepxr
заключается в том, что «можно оценить значение функции или переменной во время компиляции». Компилятор по-прежнему может выдавать инструкции для выполнения математических вычислений во время выполнения.