constexpr浮点数运算的影响是什么?

34
自从C++11以来,我们能够在编译时进行浮点数运算。C++23和C++26向一些函数添加了constexpr,但并非全部都有。 constexpr的浮点数计算在一般情况下是奇怪的,因为结果并不完全准确。然而,constexpr代码应该始终提供一致的结果。C++如何处理这个问题?
问题:
- constexpr的浮点数计算是如何工作的? - 对于所有编译器来说,结果是否相同? - 对于同一编译器的编译时和运行时,结果是否相同?
- 为什么有些函数是constexpr,而其他函数(例如std::nearbyint)不是?

1
回答:不,不,给它一些时间。 - n. m. will see y'all on Reddit
7
最大的负担肯定是交叉编译器必须对不同的浮点数体系进行constexpr处理。 - BoP
7
最大的负担肯定是对于交叉编译器来说,必须将不同的浮点数架构constexpr化。 - BoP
7
最大的负担肯定是对于交叉编译器来说,必须对不同的浮点数架构进行constexpr处理。 - undefined
1
一些很好的例子展示了添加const(或constexpr)如何改变结果:https://stackoverflow.com/q/75859098/7325599 - Fedor
显示剩余3条评论
2个回答

35
C++对于浮点数(包括float和其他浮点类型)的行为几乎没有任何限制。这可能导致结果在不同编译器之间或同一编译器的运行时/编译时评估中存在潜在的不一致性。以下是关于此问题的简要总结:
在运行时 在常量表达式中
浮点数错误,如除以零 未定义行为(UB),但编译器可能通过NaN支持
静默错误作为扩展
在常量表达式中是UB,
会导致编译器错误
舍入操作,如10.0 / 3.0 舍入模式由浮点环境控制;结果可能不同 舍入是实现定义的,
结果可能与运行时不同
通过-ffast-math和其他编译器优化的语义变化 结果可能变得不太精确或更精确;破坏了IEEE-754规范 实际上没有影响;最多是实现定义的效果
调用数学函数 错误处理和舍入与+*的算术相同 自C++23起有一些constexpr
自C++26起有一些constexpr
在编译时禁止某些错误

浮点数错误

某些操作可能会失败,例如除以零。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

结果可能不总是相同的,因为浮点环境只能在运行时更改,从而可以改变舍入模式。
C++的方法是使浮点环境的效果成为实现定义。它没有提供一种可移植的方式来控制常量表达式中的舍入。
如果使用[FENVC_ACCESS]编译指示符来启用对浮点环境的控制,则本文档不会指定对常量表达式中浮点计算的影响。
- [cfenv.syn]/Note 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);

语义会因为像-ffast-math这样的标志而发生更大的变化,它允许编译器以一种不符合IEEE-754标准的方式重新排序和优化操作。例如:
float big() { return 1e20f;}

int main() {
    std::cout << big() + 3.14f - big();
}

对于IEEE-754浮点数,加法和减法不满足交换律。我们不能将其优化为:(big() - big()) + 3.14f。结果将为0,因为3.14f的精度太小,不足以改变big()的值。然而,启用-ffast-math选项后,结果可以是3.14f

数学函数

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

无FPENV依赖/非常弱依赖(C++23中的constexpr[P05333r9]

一些功能完全独立于浮点环境,或者它们根本不会失败,例如:
- `std::ceil`(向上取整) - `std::fmax`(两个数中的最大值) - `std::signbit`(获取浮点数的符号位)
此外,还有像 `std::fma` 这样的函数,它只是组合了两个浮点操作。这些函数在编译时与 `+` 和 `*` 没有任何区别。其行为与在 C 中调用这些数学函数相同(参见C23标准的附录 F.8.4),然而,在 C++ 中,如果引发除了 `FE_INEXACT` 以外的异常、设置了 `errno` 等,则它不是一个常量表达式(参见[library.c]/3)。
弱FPENV依赖(自C++26起的constexpr)[P1383r0] 其他函数依赖于浮点环境,例如std::sqrt或std::sin。然而,这种依赖被称为“弱依赖”,因为它没有明确说明,并且它只存在于浮点数运算本身就是不精确的情况下。
允许在编译时使用+和*,但不允许具有完全相同问题的数学函数是任意的。
数学特殊函数(尚未是constexpr,可能将来会是)

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

  • std::beta
  • std::riemann_zeta
  • 以及其他许多...

强FPENV依赖(尚未实现constexpr,可能永远不会)

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

结论

总结一下,在处理constexpr数学时,标准委员会和编译器开发人员面临许多挑战。几十年的讨论才解除了某些对constexpr数学函数的限制,但我们终于取得了成果。这些限制的范围从std::fabs的任意性到std::nearbyint的必要性都有所不同。
未来我们可能会看到进一步解除限制,至少对于数学特殊函数来说是如此。

4
constconstexpr有时会影响优化选择(以及编译时评估策略和结果),例如常量传播的顺序与FP收缩到FMA,就像Clang融合乘加取决于表达式参数的常数性C++舍入行为为何(对于编译时常量)在将数学移至内联函数时发生变化?中所述。 - Peter Cordes
4
constconstexpr有时会影响优化选择(以及编译时评估策略和结果),例如常量传播的顺序与FP缩减到FMA,就像Clang融合乘加取决于表达式参数的常数性C++舍入行为为何(对于编译时常量)在将数学移至内联函数时发生变化?中所述。 - Peter Cordes
2
std::numeric_limits<float>::is_iec559()在不符合IEC559标准的实现中是否应该返回true,即通过返回NaN来处理1.0f/0.0f - supercat
2
std::numeric_limits<float>::is_iec559()在不符合IEC559标准的实现中是否应该返回true,即对1.0f/0.0f的处理结果为NaN? - supercat
@supercat 这并不是它的意图;只有在实现符合IEC 60559标准时,该值才为true。请参阅https://eel.is/c++draft/numeric.limits.members#53。然而,即使这个值为true,硬件可能仍然无法完全遵循所有操作的规范,或者数学库可能无法提供所需的最准确结果。例如,请参阅https://gcc.gnu.org/onlinedocs/gcc/Floating-point-implementation.html;GCC文档承认系统库可能不符合规范,并且这与编译器无关,尽管`is_iec559`是一个编译时常量。 - Jan Schultke
显示剩余2条评论

9

Jan Schultke已经给出了一个很好的答案,我只想解释一些可能的误解:

编译时间与constexpr不同

自从C++11以来,我们就能够在编译时进行浮点数运算。

这是不正确的。编译器早在很久之前就能够进行编译时数学运算,而且旧版本的C++中没有任何阻止这种能力的东西。即使在-std=c++98 -O0的情况下,GCC和Clang也可以愉快地进行编译时浮点数除法。

另外,需要记住的是constepxr的唯一要求是“在编译时能够评估函数或变量的值是可能的”。编译器仍然可以发出指令在运行时进行数学运算,这完全没问题。


6
这个问题可能需要进一步扩展到constevalconstinit和类似的领域。 - Deduplicator
6
问题可能应该进一步扩展到constevalconstinit和类似的领域。 - Deduplicator
6
这个问题可能应该进一步扩展到constevalconstinit和类似的领域。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接