4

我有一个非常奇怪的错误,我花了几天时间试图弄清楚,所以现在我想看看是否有人有任何评论来帮助我理解发生了什么。

一些背景。我正在开发一个软件项目,该项目涉及使用 Boost 1.45 将 C++ 扩展添加到 Python 2.7.1,因此我的所有代码都通过 Python 解释器运行。最近,我对代码进行了更改,它破坏了我们的一个回归测试。这个回归测试可能对数值波动(例如不同的机器)过于敏感,所以我应该解决这个问题。然而,由于这个回归是在产生原始回归结果的同一台机器/编译器上中断的,我将结果的差异追溯到这个数字代码片段(可验证这与我更改的代码无关):

c[3] = 0.25 * (-3 * df[i-1] - 23 * df[i] - 13 * df[i+1] - df[i+2]
               - 12 * f[i-1] - 12 * f[i] + 20 * f[i+1] + 4 * f[i+2]);
printf("%2li %23a : %23a %23a %23a %23a : %23a %23a %23a %23a\n",i,
       c[3],
       df[i-1],df[i],df[i+1],df[i+2],f[i-1],f[i],f[i+1],f[i+2]);

它构造了一些数值表。注意:

  • %a prints 提供精确的 ascii 表示
  • 左侧(lhs)是 c[3],rhs 是其他 8 个值。
  • 下面的输出是 i 的值远离 f, df 的边界
  • 该代码存在于 i 上的循环中,该循环本身嵌套了多个层(因此我无法提供一个孤立的案例来重现此代码)。

所以我克隆了我的源代码树,我编译的两个可执行文件之间的唯一区别是克隆包含一些额外的代码,这些代码甚至在这个测试中都没有执行。这让我怀疑它一定是内存问题,因为唯一的区别应该是代码在内存中的位置......无论如何,当我运行这两个可执行文件时,它们产生的差异如下:

diff new.out old.out 
655,656c655,656
<  6  -0x1.7c2a5a75fc046p-10 :                  0x0p+0                  0x0p+0                  0x0p+0   -0x1.75eee7aa9b8ddp-7 :    0x1.304ec13281eccp-4    0x1.304ec13281eccp-4    0x1.304ec13281eccp-4    0x1.1eaea08b55205p-4
<  7   -0x1.a18f0b3a3eb8p-10 :                  0x0p+0                  0x0p+0   -0x1.75eee7aa9b8ddp-7   -0x1.a4acc49fef001p-6 :    0x1.304ec13281eccp-4    0x1.304ec13281eccp-4    0x1.1eaea08b55205p-4    0x1.9f6a9bc4559cdp-5
---
>  6  -0x1.7c2a5a75fc006p-10 :                  0x0p+0                  0x0p+0                  0x0p+0   -0x1.75eee7aa9b8ddp-7 :    0x1.304ec13281eccp-4    0x1.304ec13281eccp-4    0x1.304ec13281eccp-4    0x1.1eaea08b55205p-4
>  7  -0x1.a18f0b3a3ec5cp-10 :                  0x0p+0                  0x0p+0   -0x1.75eee7aa9b8ddp-7   -0x1.a4acc49fef001p-6 :    0x1.304ec13281eccp-4    0x1.304ec13281eccp-4    0x1.1eaea08b55205p-4    0x1.9f6a9bc4559cdp-5
<more output truncated>

您可以看到 c[3] 中的值略有不同,而 rhs 值没有任何不同。因此,一些相同的输入会导致不同的输出。我尝试简化 rhs 表达式,但我所做的任何更改都会消除差异。如果我打印 &c[3],那么差异就会消失。如果我在我可以访问的两台不同的机器(linux、osx)上运行,则没有区别。这是我已经尝试过的:

  • valgrind(在 python 中报告了许多问题,但在我的代码中没有任何问题,也没有任何看起来严重的问题)
  • -D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_ASSERT -D_GLIBCXX_DEBUG_PEDASSERT -D_GLIBCXX_DEBUG_VERIFY(但没有断言)
  • -fno-strict-aliasing (但我确实从 boost 代码中得到了别名编译警告)

我尝试在有问题的机器上从 gcc 4.1.2 切换到 gcc 4.5.2,这种特定的、孤立的差异消失了(但回归仍然失败,所以让我们假设这是一个不同的问题)。

我能做些什么来进一步隔离问题吗?以供日后参考,有没有什么方法可以更快的分析或理解这类问题?例如,鉴于我对 lhs 变化的描述,即使 rhs 没有变化,你会得出什么结论?

编辑:问题完全是由于-ffast-math.

4

1 回答 1

4

您可以更改程序的浮点数据类型。如果使用float,可以切换到double;如果c, f,df是双精度,您可以切换到长双精度(intel 为 80 位;sparc 为 128)。对于 4.5.2,您甚至可以尝试使用_float128(128 位)软件模拟类型。

浮点类型越长,舍入误差就越小。

为什么添加一些代码(甚至未执行)会改变结果?如果代码大小发生变化,gcc 可能会以不同的方式编译程序。GCC 内部有很多启发式方法,一些启发式方法是基于函数大小的。所以 gcc 可能会以不同的方式编译你的函数。

此外,尝试使用标志编译您的项目,-mfpmath=sse -msse2因为使用 x87(旧 gcc 的默认 fpmath)是http://gcc.gnu.org/wiki/x87note

默认情况下 x87 算术不是真的 64/32 位 IEEE

-ffast-mathPS:当您对稳定的数字结果感兴趣时,不应使用-like 选项:http: //gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Optimize-Options.html

-ffast-math 设置 -fno-math-errno、-funsafe-math-optimizations、-fno-trapping-math、-ffinite-math-only、-fno-rounding-math、-fno-signaling-nans 和 fcx-limited-range。

此选项导致定义预处理器宏FAST_MATH

任何 -O 选项都不应打开此选项,因为它可能导致依赖于数学函数的 IEEE 或 ISO 规则/规范的精确实现的程序的错误输出。

这部分快速数学可能会改变结果

-funsafe-math-optimizations 允许对浮点运算进行优化,即 (a) 假设参数和结果是有效的,并且 (b)可能违反 IEEE 或 ANSI 标准。在链接时使用时,它可能包含更改默认 FPU 控制字或其他类似优化的库或启动文件。

这部分将向用户隐藏陷阱和类似 NaN 的错误(有时用户希望准确地获取所有陷阱来调试他的代码)

-fno-trapping-math 编译代码假设浮点运算不能生成用户可见的陷阱。这些陷阱包括被零除、上溢、下溢、不精确结果和无效操作。此选项暗示 -fno-signaling-nans。例如,如果依赖于“不间断”的 IEEE 算法,设置此选项可能会允许更快的代码。

快速数学的这一部分说,编译器可以在任何地方假定默认舍入模式(对于某些程序来说可能是错误的):

-fno-rounding-math 启用假定默认浮点舍入行为的转换和优化。对于所有浮点到整数的转换,这是四舍五入到零,对于所有其他算术截断,这是四舍五入到最近。...此选项允许在编译时对浮点表达式进行常量折叠(可能受舍入模式的影响)以及在存在符号相关舍入模式时不安全的算术转换。

于 2011-07-21T06:18:27.090 回答