constexpr 浮点数学有何含义?

Jan*_*tke 34 c++ floating-point constexpr c++23 c++26

从 C++11 开始,我们可以在编译时进行浮点数学运算。C++23 和 C++26 添加了constexpr一些函数,但不是全部。

constexpr一般来说,浮点数学很奇怪,因为结果并不完全准确。然而,constexpr代码应该始终提供一致的结果。C++ 如何解决这个问题?

问题

  • constexpr浮点数学 是如何工作的?
    • 所有编译器的结果都相同吗?
    • 对于同一编译器,编译时和运行时的结果是否相同?
  • 为什么有些功能有效constexpr,而其他功能则不然(例如std::nearbyint

Jan*_*tke 35

floatC++ 对和其他浮点类型的行为施加很少的限制。这可能会导致编译器之间以及同一编译器的运行时/编译时评估之间的结果可能不一致。这是 tl;dr 的内容:

运行时 在常量表达式中
浮点错误,例如除以零 UB,但编译器可能
通过 NaN 作为扩展支持静默错误
常量表达式中的 UB
会导致编译器错误
舍入运算,例如10.0 / 3.0 通过
浮点环境控制的舍入模式;结果可能会有所不同
舍入是实现定义的,
结果可能与运行时不同
语义通过-ffast-math
其他编译器优化而改变

结果可能会变得不太精确或更加精确;IEEE-754 一致性被破坏
实践中没有效果;至多
实现定义的效果
调用数学函数 对错误和舍入的处理与使用and
进行算术的处理相同+*
有些constexpr是从 C++23 开始,
有些constexpr是从 C++26 开始,
有些错误在编译时是不允许的

浮点错误

某些操作可能会失败,例如除以零。C++ 标准说:

如果 / 或 % 的第二个操作数为零,则行为未定义。

- [expr.mul]/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
Run Code Online (Sandbox Code Playgroud)

结果可能并不总是相同,因为浮点环境只能在运行时更改,因此可以更改舍入模式。

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);
Run Code Online (Sandbox Code Playgroud)

语义的变化甚至更多,比如-ffast-math允许编译器以不符合 IEEE-754 标准的方式重新排序和优化操作。例如:

float big() { return 1e20f;}

int main() {
    std::cout << big() + 3.14f - big();
}
Run Code Online (Sandbox Code Playgroud)

对于 IEEE-754 浮点数,加法和减法不可交换。我们无法将其优化为:(big() - big()) + 3.14f。结果将是0,因为3.14f太小而无法big()在添加时进行任何更改,因为缺乏精度。然而,-ffast-math启用后,结果可能是3.14f

数学函数

所有操作的常量表达式都可能存在运行时差异,其中包括对数学函数的调用。编译时可能与运行时std::sqrt(2)不同。std::sqrt(2)然而,这个问题并不是数学函数所独有的。您可以将这些功能分为以下几类:

无 FPENV 依赖性/非常弱的依赖性(constexpr自 C++23 起)[P05333r9]

有些函数完全独立于浮点环境,或者它们根本不会失败,例如:

  • std::ceil(四舍五入到下一个更大的数字)
  • std::fmax(两个数字中的最大值)
  • std::signbit(获取浮点数的符号位)

此外,还有一些函数std::fma只组合了两个浮点运算。这些问题并不比编译时的问题+更大*。其行为与在 C 中调用这些数学函数相同(请参阅C23 标准,附录 F.8.4FE_INEXACT ),但是,如果引发、errno设置等除外,则它不是 C++ 中的常量表达式(请参阅[library. c]/3)。

弱 FPENV 依赖性(constexpr自 C++26 起)[P1383r0]

其他函数依赖于浮点环境,例如std::sqrtstd::sin。然而,这种依赖性被称为依赖性,因为它没有明确说明,并且它的存在只是因为浮点数学本质上是不精确的。

+在编译时允许and是任意的*,但不允许具有完全相同问题的数学函数。

数学特殊函数(constexpr还没有,将来可能)

[P1383r0]认为添加数学特殊函数太过雄心勃勃,例如:constexpr

  • std::beta
  • std::riemann_zeta
  • 还有很多 ...

强烈的 FPENV 依赖性(constexpr还没有,可能永远不会)

某些函数(如std::nearbyint标准中明确声明使用当前舍入模式)。这是有问题的,因为您无法使用标准方法在编译时控制浮点环境。像这样的函数std::nearbyint不是constexpr,而且可能永远不会。

结论

总之,标准委员会和编译器开发人员在处理constexpr数学时面临着许多挑战。为了取消对数学函数的一些限制,我们经过了数十年的讨论constexpr,但我们终于实现了。这些限制的范围从任意的(在 的情况下)std::fabs到必要的(在 的情况下)std::nearbyint

未来我们可能会看到进一步的限制取消,至少对于数学特殊函数而言。

  • `const` 或 `constexpr` 有时会影响优化选择(以及编译时评估策略和结果),例如常量传播的顺序与 FP 收缩到 FMA 的顺序,如 [Clang 融合乘加取决于表达式参数的恒定性](/sf/ask/5310136891/) 和 [如果数学移至内联函数,为什么 C++ 舍入行为(对于编译时常量)会发生变化?](/sf/ 76214764)。 (4认同)
  • 在不以符合 IEC559 的方式处理“1.0f/0.0f”的实现上,“std::numeric_limits&lt;float&gt;::is_iec559()”是否应该产生“true”,即通过产生 NaN? (2认同)

G. *_*pen 9

Jan Schultke 已经给出了很好的答案,我只想解决一些潜在的误解:

\n

编译时间不一样constexpr

\n
\n

从 C++11 开始,我们可以在编译时进行浮点数学运算。

\n
\n

这不是真的。编译器能够在更长的时间内进行编译时数学计算,旧版本的 C++ 中没有任何东西可以阻止这一点。constexprGCC 和 Clang 会很乐意在不使用 . 的情况下进行编译时浮点除法-std=c++98 -O0

\n

另外,最好记住,唯一的要求constepxr是 \xe2\x80\x9cit 可以编译时评估函数或变量的值。\xe2\x80\x9d 它仍然完全没问题让编译器发出在运行时进行数学运算的指令。

\n

  • 这个问题可能应该扩展到“consteval”、“constinit”和类似的领域。 (6认同)