升级后浮点数不一致 libc/libm

Floating point inconsistencies after upgrading libc/libm

我最近将 OS 从 Debian 9 升级到 Debian 11。我有一堆服务器 运行 进行模拟,一个子集产生特定结果,另一个子集产生不同的结果。 Debian 9 以前不会发生这种情况。我制作了一个最小的失败示例:

#include <stdio.h>
#include <math.h>

int main()
{
  double lp = 11.525775909423828;
  double ap = exp(lp);

  printf("%.14f %.14f\n", lp, ap);

  return 0;
}

lp 值在每台机器上打印相同的答案,但我有两个不同的 ap 答案:101293.33662281210127 和 101293.33662281208672

代码是用“gcc fptest.c -lm -O0”编译的。刚刚添加了“-O0”以确保优化不是问题。没有这个选项,它的行为是一样的。

Debian 11版本中链接的库是libm-2.31.so和libc-2.31.so.

(工作)Debian 9 版本中链接的库是 libm-2.24.so 和 libc-2.24.so.

服务器都是 运行 不同的 CPU,所以很难说太多。但是我在 xeon E5-2695 v2 和 xeon E5-2695 v3 之间得到不同的结果,例如。

在我拥有的所有处理器中,我在 Debian 11 上只看到这两个结果之一,而在 Debian 9 上 运行ning 时,我始终只得到一个结果。

我觉得这像是 libm-2.31 and/or libc-2.31 中的错误。我对这种事情的经验为零。有人可以解释一下我所看到的是否是预期的吗?它看起来像一个错误吗?我能做些什么吗?等等

也尝试用 clang 编译,得到完全相同的问题。

另请注意,在 Debian 11 上的 Debian 9 运行s 上编译的二进制文件生成与 Debian 11 二进制文件相同的 results/problem,这进一步加重了我怀疑这是与库相关的可能性(我不能 运行 Debian 9 上的 Debian 11 二进制文件)。

更新

刚刚阅读 this post 很有帮助。所以我很高兴不同的架构可能会为 exp() 函数提供不同的结果。但我所有的处理器都是 x86_64 和某种英特尔 xeon-xxxx。我不明白为什么具有完全相同库的完全相同的二进制文件在不同的处理器上给出不同的结果。

正如 post 中所建议的那样,我使用 %a 打印了这些值。这两个答案仅在 LSB 上有所不同。 如果我使用 expl() 我会在所有机器上得到相同的答案。

解释为什么我看到差异,如果这是预期的,那就太好了。任何确保一致性的编译器标志也很好。

这不是错误。浮点运算有舍入误差。对于单个算术运算 + - * / sqrt 结果应该是相同的,但是对于 floating-point 函数你不能真正期望它。

在这种情况下,编译器本身似乎在编译时产生了结果。您使用的处理器不太可能产生影响。我们不知道新版本是否比旧版本更精确。

背景

澄清 e11.5257759094238_28.

结果的一些注释

11.5257759094238_28 不能完全表示为 double,而是使用附近的值 0x1.70d328p+3 或正好是 11.5257759094238_28125,所以我们真的正在调查 e11.5257759094238_28125 以及 exp() 的表现如何。

我们有 3 个结果,2 个来自不同的编译器 exp(),第 3 个是数学结果 . 2 个 C 代码答案相隔 1 ULP,数学答案在它们之间,

                                              0072831    difference from math
0x1.8bad562ce9a10      p+16  101293.336622812 0867163... OP's smaller answer
0x1.8bad562ce9a10802...p+16  101293.336622812 0939994... Math
0x1.8bad562ce9a11      p+16  101293.336622812 1012682... OP's larger answer
                                              0072688    difference from math

数学答案几乎是两个可表示 double 之间的一半,其中较大的 OP 答案接近 1/1000 ULP。

差异化的候选贡献者

  • 用户代码和数学库与 OP 假设的不完全相同。 (IMO - 最有可能)

  • 代码使用处理器内置的 e(x) 硬件原语指令,这些指令会呈现不同的答案。 (可能)

  • 继承的舍入模式不一样(存疑)


在这种情况下,获得一致的答案 - 直到最后一位 - 非常具有挑战性。一个常见的问题是较新的处理器和支持库倾向于呈现比以前 更好 的答案。这是 Table-maker's_dilemma 的一部分,测试代码应该容忍 exp().

等超越函数中的这些微小(<= 1.0 ULP)差异

请注意,使用 long double 可能会 ,因为某些实现的 long double 编码与 double 完全相同。

除了少数例外,glibc 的目标不是正确舍入数学函数。

这意味着不同的 glibc 版本和不同的处理器之间的结果可能略有不同。处理器差异可能源于基于处理器功能的不同实现,例如取决于处理器是否实现 FMA 指令。

终于明白是怎么回事了。

在我的代码开始之前,libc_start_main 调用了 ieee754_exp_ifunc。这个函数似乎 select 哪个函数执行 exp()。在一组机器上它 selects ieee754_exp_avx 而在另一台机器上设置它 selects ieee754_exp_fma.

ieee754_exp_ifunc 对 dl_x86_cpu_features 有评论,因此似乎是根据处理器能力选择指数函数实现。我没有进一步挖掘。

在 Debian 11 中,在具有 avx 和 fma 功能的机器上,选择 fma。对于我没有 fma 的机器子集,它显然使用 avx。然而,在 Debian 9 中,它甚至在具有 fma 的处理器上使用 avx。

最后,我尝试使用“-mavx”使用 gcc 进行编译。它给出了一个很好的忽略并且仍然在拥有它的机器上使用 fma。我不知道if/how我可以强制使用avx吗?我想这是另一个 post.

的问题