4

我最近将我的操作系统从 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 运行,因此很难说太多。但是例如,我在至强 E5-2695 v2 和至强 E5-2695 v3 之间得到了不同的结果。

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

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

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

另请注意,在 Debian 9 上编译的二进制文件在 Debian 11 上运行,并产生与 Debian 11 二进制文件相同的结果/问题,这进一步加重了我对库相关的怀疑(我无法在 Debian 9 上运行 Debian 11 二进制文件)。

更新

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

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

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

4

4 回答 4

4

背景

一些说明以澄清e 11.5257759094238_28的结果。

11.5257759094238_28 不能完全表示为double,而是使用 0x1.70d328p+3 的附近值或恰好 11.5257759094238_28125,因此我们确实在调查e 11.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

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

候选贡献者的差异

  • 用户代码和数学库与 OP 所设想的并不完全相同。(国际海事组织 - 最有可能)

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

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


在这种情况下,获得一致的答案 - 直到最后一点 - 非常具有挑战性。一个常见问题是较新的处理器和支持库往往会提供比以前更好的答案。这是Table-maker's_dilemma的一部分,测试代码应该能够容忍超越函数(如exp().


请注意,使用long double可能无济于事,因为某些实现的long double编码与double.

于 2022-02-28T21:06:43.550 回答
4

终于搞清楚是怎么回事了。

在我的代码开始之前,libc_start_main 调用了 ieee754_exp_ifunc。此函数似乎选择执行 exp() 的函数。在一组机器上,它选择 ieee754_exp_avx,而在另一组机器上,它选择 ieee754_exp_fma。

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

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

最后,我尝试使用“-mavx”使用 gcc 进行编译。它很好地忽略了这一点,并且仍然在拥有它的机器上使用 fma。我想知道我是否/如何强制 avx?我想这是另一个帖子的问题。

于 2022-03-01T13:31:27.407 回答
3

除了少数例外,glibc 并不针对正确舍入的数学函数。

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

于 2022-02-28T21:24:08.023 回答
-1

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

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

于 2022-02-28T13:18:36.933 回答