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) * x到1.0f这意味着(1.0f + eps) * x - x将给出较小的值。然而,机器精度是 的1.0f两倍,0xFFFFFFp-24f因此保留额外精度的乘加指令之类的指令将具有大致的中间值,1.0 + 0.5 * eps这将产生更大的值。
我不明白的是,为什么答案会根据数学是在内联函数中还是直接调用而变化。标准中是否有某个地方对此进行了合理化,这是未定义的行为,还是 Clang 错误?
使用 clang 16 的默认值-ffp-contract=on(如#pragma STDC FP_CONTRACT ON),ISO C++ 允许编译器为 FP 临时值保持无限精度或不保持无限精度,由其选择,包括根据具体情况进行选择。值得注意的是,承包a*b+c给fma(a,b,c). 这包括在编译时进行常量传播时。ISO C++ 允许编译指示的任一默认值。
也可以看看
如果您使用-ffp-contract=off(或令人惊讶地-ffp-contract=fast),两个函数都会返回5.96046448E-8。fma((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,也许当它不必跟踪操作是否是源中同一语句的一部分时,它可以推迟寻找收缩的优化过程。将其推迟到内联和恒定传播之后可以解释这样一个事实:恒定传播的方式与禁用收缩时的结果相同。