提问人:Jan Schultke 提问时间:7/4/2023 更新时间:7/5/2023 访问量:2288
constexpr 浮点数学的含义是什么?
What are the implications of constexpr floating-point math?
问:
从 C++11 开始,我们能够在编译时进行浮点数学运算。C++23 和 C++26 添加到一些函数中,但不是全部。constexpr
constexpr
浮点数学通常很奇怪,因为结果并不完全准确。但是,代码应该始终提供一致的结果。C++ 如何处理这个问题?constexpr
问题
- 浮点数学是如何工作的?
constexpr
- 所有编译器的结果都一样吗?
- 同一编译器的编译时和运行时之间的结果是否相同?
- 为什么有些函数是 ,而另一些则不是 (比如
constexpr
std::nearbyint
)
答:
C++ 对浮点类型的行为施加了很少的限制。这可能会导致编译器之间以及同一编译器的运行时/编译时评估之间的结果可能不一致。这是 tl;博士:float
在运行时 | 在常量表达式中 | |
---|---|---|
浮点错误,例如除以零 | UB,但编译器可能通过 NaN 作为扩展来支持 静默错误 |
常量表达式 中的 UB 会导致编译器错误 |
四舍五入操作,如 10.0 / 3.0 |
通过 浮点环境控制舍入模式;结果可能会有所不同 |
舍入是实现定义的, 结果可能与运行时不同 |
通过 -ffast-math 和其他编译器优化进行语义更改 |
结果可能会因此变得不那么精确或更精确 ;IEEE-754 一致性被打破 |
在实践中没有效果;最多 实现定义的效果 |
对数学函数的调用 | 错误和舍入的处理 与使用 和 的算术相同 + * |
有些从 C++23 开始,有些从 C++26 开始, 有些错误在编译时是不允许的 constexpr constexpr |
浮点错误
某些操作可能会失败,例如除以零。C++标准说:
如果 / 或 % 的第二个操作数为零,则行为未定义。
在常量表达式中,这是遵循的,因此不可能通过操作生成 NaN 或在编译时引发FE_DIVBYZERO
。
浮点数也不例外。但是,当 是 时,大多数编译器将具有 IEEE-754 合规性作为扩展。例如,允许除以零,并根据操作数产生无穷大或 NaN。std::numeric_limits<float>::is_iec559()
true
舍入模式
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 的方式重新排序和优化操作。例如:-ffast-math
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 以来)[P05333r9]constexpr
有些函数完全独立于浮点环境,或者它们根本不会失败,例如:
std::ceil
(四舍五入到下一个较大的数字)std::fmax
(最多两个数字)std::signbit
(获取浮点数的符号位)
此外,还有一些函数只是结合了两个浮点运算。这些并不比编译时更成问题。该行为与在 C 中调用这些数学函数相同(参见 C23 标准,附录 F.8.4),但是,如果引发、设置了除之外的异常等,则它不是 C++ 中的常量表达式(参见 [library.c]/3)。std::fma
+
*
FE_INEXACT
errno
弱 FPENV 依赖性(自 C++26 以来)[P1383r0]constexpr
其他函数依赖于浮点环境,例如 或 。然而,这种依赖性被称为弱依赖性,因为它没有明确说明,它的存在只是因为浮点数学本质上是不精确的。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
未来,我们可能会看到进一步的限制被取消,至少对于数学特殊函数。
评论
const
或者有时会影响优化选择(以及编译时评估策略和结果),例如常数传播与 FP 收缩到 FMA 的顺序,如 Clang 融合乘加取决于表达式参数的恒定性和为什么如果数学移动到内联函数,C++ 舍入行为(对于编译时常量)会改变?。constexpr
std::numeric_limits<float>::is_iec559()
true
1.0f/0.0f
is_iec559
Jan Schultke已经给出了一个很好的答案,我只想解决一些潜在的误解:
编译时间与constexpr
从 C++11 开始,我们能够在编译时进行浮点数学运算。
那不是真的。编译器已经能够进行编译时数学运算的时间要长得多,而旧版本的 C++ 中没有任何内容可以阻止这一点。GCC 和 Clang 很乐意在没有 的情况下进行编译时浮点除法,即使使用 .constexpr
-std=c++98 -O0
此外,最好记住,constepxr
的唯一要求是“可以在编译时评估函数或变量的值”。编译器在运行时发出指令来执行数学运算仍然完全没问题。
评论
consteval
constinit
评论
constexpr
const
constexpr