Каковы последствия математики с плавающей запятой constexpr?

Начиная с C++11, мы можем выполнять математические операции с плавающей запятой во время компиляции. В C++23 и C++26 добавлены некоторые функции, но не все.

Математика с плавающей запятой вообще странная, потому что результаты не совсем точны. Однако предполагается, что код всегда обеспечивает согласованные результаты. Как C++ подходит к этой проблеме?

Вопросы

  • Как работает математика с плавающей запятой?
    • Одинаковы ли результаты для всех компиляторов?
    • Одинаковы ли результаты во время компиляции и во время выполнения одного и того же компилятора?
  • Почему некоторые функцииconstexpr, а другие нет (например,std::nearbyint)

2 ответа

C++ накладывает очень мало ограничений на поведениеfloatи другие типы с плавающей запятой. Это может привести к возможным несоответствиям результатов как между компиляторами, так и между оценками во время выполнения/компиляции одним и тем же компилятором. Вот информация об этом:

Ошибки с плавающей запятой

Некоторые операции могут завершиться неудачей, например деление на ноль. Стандарт C++ гласит:

Если второй операнд / или % равен нулю, поведение не определено.

- [выражение.мул]/4

В константных выражениях это соблюдается, поэтому невозможно получить 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] используется для включения управления средой с плавающей запятой, в этом документе не указано влияние на вычисление с плавающей запятой в константных выражениях.

- [cfenv.syn]/Примечание 1

Оптимизация компилятора

Во-первых, компиляторы могут захотеть оптимизировать ваш код, даже если это меняет его смысл. Например, 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заключается в том, что «можно оценить значение функции или переменной во время компиляции». Компилятор по-прежнему может выдавать инструкции для выполнения математических вычислений во время выполнения.

Другие вопросы по тегам