GCC启用-flto与-ffast-math时pow函数计算结果不一致问题
近期我遇到一个与LTO(链接时优化)、-ffast-math编译选项相关的异常现象:无论是否启用-flto选项,cmath库中pow函数的调用返回结果存在不一致。
$ g++ --version g++ (GCC) 8.3.0 Copyright (C) 2018 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ ll /lib64/libc.so.6 lrwxrwxrwx 1 root root 12 Sep 3 2019 /lib64/libc.so.6 -> libc-2.17.so $ ll /lib64/libm.so.6 lrwxrwxrwx 1 root root 12 Sep 3 2019 /lib64/libm.so.6 -> libm-2.17.so $ cat /etc/redhat-release CentOS Linux release 7.5.1804 (Core)
测试代码
fixed.hxx
#include <cstdint> double Power10f(const int16_t power);
fixed.cxx
#include "fixed.hxx" #include <cmath> double Power10f(const int16_t power) { return pow(10.0, (double) power); }
test.cxx
#include <iostream> #include <cmath> #include <iomanip> #include <cstdint> #include "fixed.hxx" int main(int argc, char** argv) { if (argc >= 3) { int64_t value = (int64_t)atoi(argv[1]); int16_t power = (int16_t)atoi(argv[2]); double x = Power10f(power); std::cout.precision(17); std::cout << std::scientific << x << std::endl; std::cout << std::scientific << (double)value * x << std::endl; return 0; } return 1; }
编译运行结果
使用-ffast-math选项编译代码时,是否添加-flto选项会得到完全不同的运行结果:
- 启用
-flto时,程序最终调用__pow_finite版本的函数,返回精度更高的“准确”结果:
$ g++ -O3 -DNDEBUG -ffast-math -std=c++17 -flto -o fixed.cxx.o -c fixed.cxx $ g++ -O3 -DNDEBUG -o fdtest fixed.cxx.o test.cxx $ ./fdtest 81 20 1.00000000000000000e+20 8.10000000000000000e+21 $ objdump -DC fdtest > fdtest.dump $ cat fdtest.dump ... 0000000000400930 <Power10f(short)>: 400930: 0f bf ff movswl %di,%edi 400933: 66 0f ef c9 pxor %xmm1,%xmm1 400937: f2 0f 10 05 99 00 00 movsd 0x99(%rip),%xmm0 # 4009d8 <_IO_stdin_used+0x8> 40093e: 00 40093f: f2 0f 2a cf cvtsi2sd %edi,%xmm1 400943: e9 d8 fd ff ff jmpq 400720 <__pow_finite@plt> 400948: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40094f: 00 ...
- 未启用
-flto时,程序最终调用__exp_finite(推测为-ffast-math开启的优化转换,将pow转换为exp计算实现),返回存在精度误差的“不准确”结果:
$ g++ -O3 -DNDEBUG -ffast-math -std=c++17 -o fixed.cxx.o -c fixed.cxx $ g++ -O3 -DNDEBUG -o fdtest fixed.cxx.o test.cxx $ ./fdtest 81 20 1.00000000000000786e+20 8.10000000000006396e+21 $ objdump -DC fdtest > fdtest.dump $ cat fdtest.dump ... 0000000000400930 <Power10f(short)>: 400930: 0f bf ff movswl %di,%edi 400933: 66 0f ef c0 pxor %xmm0,%xmm0 400937: f2 0f 2a c7 cvtsi2sd %edi,%xmm0 40093b: f2 0f 59 05 95 00 00 mulsd 0x95(%rip),%xmm0 # 4009d8 <_IO_stdin_used+0x8> 400942: 00 400943: e9 88 fd ff ff jmpq 4006d0 <__exp_finite@plt> 400948: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40094f: 00 ...
请问上述现象是GCC的预期设计行为,还是我的代码写法存在问题导致的异常结果?
该现象在其他平台也可复现,例如搭载g++ 12.1、glibc 2.35的ArchLinux系统。
问题解答
这是-ffast-math开启后的预期行为,和代码写法无关。
-ffast-math不是单一优化开关,它是一组放宽IEEE 754浮点标准约束的选项集合,核心设计目标就是用可接受的精度损失换计算性能,其中明确允许编译器对标准数学函数做等价转换——你观察到的非LTO编译下pow(10.0, x)被替换为exp(x * ln(10))就是这类优化的典型:转换后的计算路径速度更快,但因为多了一次乘法和常数ln(10)的存储精度损失,结果和直接调用pow存在微小误差,完全在-ffast-math允许的误差范围内。
两种编译模式下的行为差异,来自GCC不同优化阶段的规则不统一:
- 无LTO编译时,GCC在单编译单元(
fixed.cxx)的中端优化阶段就能识别到固定底数的pow调用模式,直接完成pow到exp的转换,后续链接阶段不会修改已经生成的函数调用逻辑,最终就会走到__exp_finite的实现。 - 加
-flto编译时,你在链接阶段没有追加-flto和-ffast-math参数,GCC不会启动跨单元LTO优化流程,直接使用了目标文件里生成的保守调用路径,也就是直接调用__pow_finite,没有做激进的函数转换。哪怕你链接阶段补全参数,不同GCC版本的LTO优化阶段对pow转exp这类转换的触发条件判断也和单编译单元阶段不完全一致,依然可能出现结果差异。
只要开启了-ffast-math,GCC本身就不承诺不同优化等级、不同编译参数下浮点计算结果的位精确一致性,这类差异不属于bug。如果你的业务场景要求浮点结果可复现、严格符合IEEE 754精度要求,不要直接开启全套-ffast-math;如果必须使用快速数学优化,又需要固定pow的计算行为,可以单独给相关函数加__attribute__((optimize("no-fast-math")))属性关闭局部的快速数学优化,或者按需开启-fno-math-errno这类粒度更细的子选项,避免不必要的精度损失。
内容的提问来源于stack exchange,提问作者Liu Wei

