如果数学移至内联函数,为什么 C++ 舍入行为(对于编译时常量)会发生变化?

use*_*116 7 c++ floating-point clang compiler-optimization clang++

考虑以下函数:

static inline float Eps(const float x) {
  const float eps = std::numeric_limits<float>::epsilon();
  return (1.0f + eps) * x - x;
}

float Eps1() {
  return Eps(0xFFFFFFp-24f);
}

float Eps2() {
  const float eps = std::numeric_limits<float>::epsilon();
  const float x = 0xFFFFFFp-24f;
  return (1.0f + eps) * x - x;
}
Run Code Online (Sandbox Code Playgroud)

-O2with中-std=c++20,这两个函数都编译为一个函数,movss后跟一个ret针对 x86 的 using clang 16.0.0 和mov一个bx针对 ARM 的 with gcc 11.2.1。为 ARM 生成的程序集与返回值 ~5.96e-8 一致,但为 x86 生成的程序集则不然。Eps1()(使用内联函数)返回〜1.19e-7,同时Eps2()返回〜5.96e-8。[编译器资源管理器 / Godbolt ]

.LCPI0_0:
  .long 0x33ffffff # float 1.19209282E-7
Eps1(): # @Eps1()
  movss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
  ret
.LCPI1_0:
  .long 0x33800000 # float 5.96046448E-8
Eps2(): # @Eps2()
  movss xmm0, dword ptr [rip + .LCPI1_0] # xmm0 = mem[0],zero,zero,zero
  ret
Run Code Online (Sandbox Code Playgroud)

我可以理解编译器选择任一选项。对于x = 0xFFFFFFp-24f(即下面的下一个可表示值1.0f),两个编译器都会一致舍入(1.0f + eps) * x1.0f这意味着(1.0f + eps) * x - x将给出较小的值。然而,机器精度是 的1.0f两倍,0xFFFFFFp-24f因此保留额外精度的乘加指令之类的指令将具有大致的中间值,1.0 + 0.5 * eps这将产生更大的值。

我不明白的是,为什么答案会根据数学是在内联函数中还是直接调用而变化。标准中是否有某个地方对此进行了合理化,这是未定义的行为,还是 Clang 错误?

Pet*_*des 8

使用 clang 16 的默认值-ffp-contract=on(如#pragma STDC FP_CONTRACT ON),ISO C++ 允许编译器为 FP 临时值保持无限精度或不保持无限精度,由其选择,包括根据具体情况进行选择。值得注意的是,承包a*b+cfma(a,b,c). 这包括在编译时进行常量传播时。ISO C++ 允许编译指示的任一默认值。

也可以看看

如果您使用-ffp-contract=off(或令人惊讶地-ffp-contract=fast),两个函数都会返回5.96046448E-8fma((1.0f + eps), x, -x)我没有检查过,但这可能与步骤后的舍入相同*x

奇怪的怪癖是,在编译时 eval 期间是否使用内联函数进行舍入不同。

使用运行时变量float x函数 arg,它为 Eps1 和 Eps2 生成相同的 asm,vfmsub132ss当您使用-march=haswell或进行编译-march=x86-64-v3以使 FMA 可用时,将两者收缩为 , 。

如果您想编写在步骤之间确实进行舍入的源代码(除了 with fp-contract=fast),请不要将多个内容作为同一表达式的一部分。https://godbolt.org/z/15T5jcdfv使用单独的语句显示 Eps3,给出与 Eps2 匹配但不与 Eps1 匹配的返回值(带有-ffp-contract=on)。


关于内联函数差异原因的理论

可能 clang / LLVM 的内部结构在内联之前将内联函数收缩到 FMA 中,因此 Eps1 通过 FMA 进行持续传播。

但在 Eps2 中,常数立即可用,因此可以先进行常数传播。对于编译器来说,一次插入第一个操作比优化抽象操作更便宜。事实上,它确实做到了这一点,而无需寻找机会进行 FMA。

https://godbolt.org/z/7c9EYK8fb显示带有float x函数参数的版本,合约打开/关闭,确认当启用 FP 收缩时,Eps1 和 Eps2 确实使用 FMA 编译为相同的 asm。(并且 Eps3 不会像预期的那样收缩。除非您使用-ffp-contract=fast。)

至于为什么-ffp-contract=fast给出与 相同的常量传播结果-ffp-contract=off,也许当它不必跟踪操作是否是源中同一语句的一部分时,它可以推迟寻找收缩的优化过程。将其推迟到内联和恒定传播之后可以解释这样一个事实:恒定传播的方式与禁用收缩时的结果相同。