在尝试处理浮点算术问题时,我遇到了一些有点混乱的事情。
首先是代码。我已将问题的本质提炼到这个例子中:
#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;
}
xinum
和xiden
应具有相同的值(当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的评估与我的处理器评估有何不同?
我确实看过this related question询问GDB将sqrt(3)
评估为0,但这不应该是一回事,因为这里没有涉及函数调用。
答案 0 :(得分:5)
可能是因为x86 FPU在寄存器中以80位精度工作,但在将值存储到存储器时会舍入到64位。 GDB将在(解释)计算的每一步都存储到内存中。
答案 1 :(得分:4)
GDB的运行时表达式评估系统肯定不能保证为您的浮点运算执行与编译器生成的优化和重新排序的机器代码相同的有效机器代码,以计算相同符号表达式的结果。实际上,保证不会执行相同的机器代码来计算给定表达式z.d * (1 - tau.d)
的值,因为这可能被视为程序的一个子集,在运行时以某些任意方式执行隔离表达式求值, “符号正确”的方式。
由于优化(替换,重新排序,子表达式消除等),浮点代码生成和CPU实现其输出特别容易与其他实现(例如运行时表达式求值程序)的符号不一致,选择指令,寄存器分配的选择和浮点环境。如果你的代码片段在临时表达式中包含许多自动变量(正如你的那样),那么代码生成具有特别大的自由度甚至零优化传递,并且具有这种自由度 - 在这种情况下 - 会失去精度以最不重要的方式出现不一致的方式。
您不会深入了解为什么GDB的运行时评估程序执行它所做的任何指令而无需深入了解GDB源代码,构建设置以及自己编译时生成的代码。
您可以针对您的程序在生成的程序集中达到最高点,以了解最终存储到z
,tau
和[对比] xiden
的工作方式。导致这些商店的浮点运算的数据流可能不像看起来那样。
通过禁用所有编译器优化(例如,GCC上的-O0
)并重写浮点表达式以不使用临时/自动变量,尝试使代码生成更具确定性。然后在GDB中的每一行中断并进行比较。
我希望我可以告诉你为什么尾数的最低有效位被翻转,但实际情况是,处理器甚至“不知道”为什么有些东西会被携带,而其他东西不会由于,例如,没有完整指令的评估顺序以及代码和GDB本身的数据跟踪。
答案 2 :(得分:2)
它不是GDB与处理器,而是处理器的内存。 x64处理器存储的精度比实际存储的精度高(80ish对64位)。只要它保留在CPU和寄存器中,它就会保留80位的准确度,但是当它被发送到内存时将确定何时以及它如何被舍入。如果GDB将所有间歇计算结果发送出CPU(我不知道是否是这种情况,或者任何接近的情况),它将在每个步骤进行舍入,这会导致稍微不同的结果。