为什么GDB在处理浮点运算时与C++不同?

16

在处理一个浮点数算术问题时,我遇到了一些令人困惑的事情。

首先,这是代码。我将我的问题的本质浓缩成了这个例子:

#include <iostream>
#include <iomanip>

using namespace std;
typedef union {long long ll; double d;} bindouble;

int main(int argc, char** argv) {
    bindouble y, z, tau, xinum, xiden;
    y.d = 1.0d;
    z.ll = 0x3fc5f8e2f0686eee; // double 0.17165791262311053
    tau.ll = 0x3fab51c5e0bf9ef7; // double 0.053358253178712838
    // xinum = double 0.16249854626123722 (0x3fc4ccc09aeb769a)
    xinum.d = y.d * (z.d - tau.d) - tau.d * (z.d - 1);
    // xiden = double 0.16249854626123725 (0x3fc4ccc09aeb769b)
    xiden.d = z.d * (1 - tau.d);
    cout << hex << xinum.ll << endl << xiden.ll << endl;
}

xinumxiden应该在y == 1时具有相同的值,但由于浮点数四舍五入错误,它们的值不同。这部分我理解。

当我用GDB来追踪代码中的差异时,就会出现问题。如果我使用GDB来重现代码中进行的评估,那么对于xiden它会给出不同的结果:

$ gdb mathtest
GNU gdb (Gentoo 7.5 p1) 7.5
...
This GDB was configured as "x86_64-pc-linux-gnu".
...
(gdb) break 16
Breakpoint 1 at 0x4008ef: file mathtest.cpp, line 16.
(gdb) run
Starting program: /home/diazona/tmp/mathtest 
...
Breakpoint 1, main (argc=1, argv=0x7fffffffd5f8) at mathtest.cpp:16
16          cout << hex << xinum.ll << endl << xiden.ll << endl;
(gdb) print xiden.d
$1 = 0.16249854626123725
(gdb) print z.d * (1 - tau.d)
$2 = 0.16249854626123722

如果我让GDB计算z.d * (1 - tau.d),你会发现它返回0.16249854626123722 (0x3fc4ccc09aeb769a),而实际上在程序中计算相同东西的C++代码返回的是0.16249854626123725(0x3fc4ccc09aeb769b)。因此,GDB在浮点运算方面使用了不同的评估模型。有人能否对此提供更多解释?GDB的评估方式与我的处理器评估方式有何不同?

我确实看过这个相关问题,其中询问为什么GDB将sqrt(3)计算为0,但这并不应该是同样的问题,因为这里没有涉及到函数调用。

3个回答

5
可能是因为x86 FPU在寄存器中工作时可以精确到80位,但将值存储到内存时会舍入到64位。在每一步(解释性的)计算中,GDB都会将值存储到内存中。

实际上,gdb的结果在数学上更正确,因此看起来gdb使用了FPU更大的精度,而g++可能使用了SSE指令。 - Daniel Fischer

4
GDB的运行时表达式评估系统不能保证执行与编译器生成的优化和重新排序的机器代码相同,以计算相同符号表达式结果的浮点运算。实际上,它保证不会执行相同的机器代码来计算给定表达式的值z.d * (1 - tau.d),因为这可能被认为是程序的子集,在其中以一种任意的“符号正确”的方式执行孤立的表达式评估。
由于优化(替换、重新排序、子表达式消除等)、指令选择、寄存器分配和浮点环境等原因,浮点代码生成及其由CPU实现的输出特别容易出现与其他实现(如运行时表达式评估器)的符号不一致。如果您的片段包含许多临时表达式中的自动变量(如您的片段),即使没有进行任何优化处理,代码生成也具有极大的自由度。而这种自由度就带来了在某些情况下失去精度的机会,从而导致看起来不一致的最低有效位。
如果您没有深入了解GDB源代码、构建设置和其自己的编译时生成的代码,就无法深入了解GDB的运行时评估器执行了什么指令。
您可以查看过程的生成汇编,以了解最终存储到ztau和[相比之下] xiden的方式。导致这些存储的浮点运算的数据流可能并非看起来那么简单。
更容易的方法是通过禁用所有编译器优化(例如,在GCC上使用-O0)并重写浮点表达式以不使用临时变量/自动变量来使代码生成更具确定性。然后在GDB中每行断点并进行比较。
我希望我能告诉您为什么曼蒂萨的最低有效位翻转,但事实上,处理器甚至“不知道”为什么某些东西会携带一个位,而其他东西则没有,例如由于评估顺序的原因,没有完整的指令和数据跟踪,包括您的代码和GDB本身。

2

这不是GDB与处理器的对比,而是内存与处理器的对比。x64处理器存储的精度比内存实际持有的位数更多(大约80位比64位)。只要它停留在CPU和寄存器中,它就保留着大约80位的精度,但当它被发送到内存时,会决定何时以及如何进行舍入。如果GDB将所有间歇性计算结果发送出CPU(我不知道是否是这种情况,或者是否接近),它将在每个步骤上进行舍入,这会导致略微不同的结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接