从float到double的转换产生不同的结果 - 相同的代码,相同的编译器,相同的OS

时间:2013-11-21 15:55:07

标签: c assembly x86 vc6 disassembly

修改:有关答案的更新,请参阅问题的结尾。

我花了几周时间跟踪一个软件中的一个非常奇怪的错误 保持。长话短说,有一个旧的软件 分发,以及需要匹配输出的新软件 旧。这两个人(在理论上)依赖于一个共同的图书馆。[1]但是,我不能 复制原始版本库生成的结果, 即使两个版本的库的源匹配。实际上 有问题的代码非常简单。原始版本看起来像这样( "伏都"评论不是我的:[2]

// float rstr[101] declared and initialized elsewhere as a global

void my_function() {
    // I have elided several declarations not used until later in the function
    double tt, p1, p2, t2;
    char *ptr;

    ptr = NULL;
    p2 = 0.0;
    t2 = 0.0; /* voooooodoooooooooo */

    tt = (double) rstr[20];
    p1 = (double) rstr[8];

    // The code goes on and does lots of other things ...
}

我所包含的最后一个陈述是不同行为的出现。在里面 原始程序rstr[8]具有值101325.,并在将其转换为之后 double [3]并对其进行分配,p1的值为101324.65625。同样,tt 最终得到值373.149999999996。我用这个确认了这些值 调试打印和检查调试器中的值(包括检查) 十六进制值)。这在任何意义上都不足为奇,它与预期的一样 浮点值。

在围绕同一版本库(以及任何调用)的测试包装器中 到重构版本的库),第一个任务(到tt) 产生相同的结果。 但是 p1最终为101325.0,与原始版本相匹配 rstr[8]中的值。这种差异虽小,但有时会产生实质性的影响 计算中的变化取决于p1的值。

我的测试包装很简单,并且与原始包含模式相匹配 确切地说,但消除了所有其他背景:

#include "the_header.h"

float rstr[101];
int main() {
    rstr[8] = 101325.;
    rstr[20] = 373.15;

    my_function();
}

出于绝望,我甚至煞费苦心地看着 由VC6生成的反汇编。

4550:   tt = (double) rstr[20];
0042973F   fld         dword ptr [rstr+50h (006390a8)]
00429745   fstp        qword ptr [ebp-0Ch]
4551:   p1 = (double) rstr[8];
00429748   fld         dword ptr [rstr+20h (00639078)]
0042974E   fstp        qword ptr [ebp-14h]

由VC6为相同的库函数生成的版本 测试代码包装器(与我重构的VC6生成的版本匹配 库的版本):

60:       tt = (double) rstr[20];
00408BC8   fld         dword ptr [_rstr+50h (0045bc88)]
00408BCE   fstp        qword ptr [ebp-0Ch]
61:       p1 = (double) rstr[8];
00408BD1   fld         dword ptr [_rstr+20h (0045bc58)]
00408BD7   fstp        qword ptr [ebp-14h]

我能看到的唯一区别,除了内存中存储数组的位置和 这个程序到底发生了多远,是领先的_ 在第二个中引用rstr。通常,VC6使用前导下划线 使用函数进行名称修改,但我找不到它的任何文档 使用数组指针进行名称修改。我也无法理解为什么这些会产生 在任何情况下都会有不同的结果,除非涉及到名称错误 以不同的方式读取从指针访问的数据。

我可以在两者之间找出唯一的其他区别(除了打电话 context)是原来是一个基于MFC的Win32应用程序,而 后者是非MFC控制台应用程序。另外两个配置了 同样的方式,它们是用相同的编译标志构建的 相同的C运行时。

我们非常感谢任何建议。


编辑:正如several answers非常有帮助地指出的那样,解决方案是检查二进制/十六进制值并对它们进行比较,以确保我认为的是事实上完全相同相同。事实证明并非如此 - 尽管如此,我仍然采取强烈抗议。

在这里,我可以吃一些不起眼的馅饼,并承认,当我思考我已经检查了这些值时,我实际上已经检查了一些其他的,密切相关的值 - 这一点我发现只有当我去的时候回来再看看数据。事实证明,rstr[8]中设置的值非常略有不同,因此转换为double会突出显示非常小的差异,然后这些差异会在整个程序中传播我注意到的方式。

初始化的差异我可以根据两个程序的工作方式来解释。具体来说,在一种情况下rstr[8]是基于GUI的用户输入指定的(在这种情况下也是转换计算的结果),而在另一种情况下,它是从已经存在的文件中读入的存储有一些精度损失。有趣的是,在任何一种情况下,它实际上都不是完全 101325.0,即使是从文件中读取它的情况,它也被存储为1.01325e5

这将教会我仔细检查我对这些事情的双重检查。非常感谢Eric Postpischilunwind提示我再次检查并及时提供反馈。它非常很有帮助。


脚注

  1. 实际上,原来的"库"是一个包含所有的头文件 内联完成的实现。标题是通过#include和。{ 通过extern语句引用的函数。我把它修好了 重构版本的库实际上是一个库,但请参阅 其余的问题。
  2. 请注意,变量名称不是我的,并且非常糟糕。同样的 使用全局变量,这在这个软件中很猖獗。我离开了 在/* voooooodoooooooooo */评论中,因为它说明了...... 不寻常......我的前任的编程实践。我认为那个元素是 因为这最初是从Fortran和开发人员翻译而来的 曾经用它来处理某种内存错误。这条线有 对代码的实际行为没有任何影响。
  3. 我很清楚,实际上并不需要在这里演员,但是这个 原始库是如何工作的,我无法修改它。

4 个答案:

答案 0 :(得分:6)

此:

  

在原始程序中,rstr[8]的值为101325.,在将其转换为double[3]并分配后,p1的值为101324.65625

暗示float值实际上并非完全是101325.0,因此当您转换为double时,您会看到更多精度。我会(高度)怀疑你在检查float值,自动(隐式和静默)舍入时使用浮点数非常常见的方法。检查位模式并使用系统中已知的浮点格式对其进行解码,以确保您不会被欺骗。

答案 1 :(得分:4)

可能性是:

  1. 尽管有报告的观察结果,rstr[8]在分配到p1之前的原始程序中的值为101324.65625,而不是报告的101325.
  2. 尽管有报道的观察结果,p1在作业后立即没有值101324.65625。
  3. 该程序未正确执行分配(包括转换为double)。
  4. 要测试1,仔细在作业之前立即检查rstr[8]的值。我建议:

    • 将值打印或记录到20位有效数字,
    • 打印或记录包含rstr[8]的字节,然后解释IEEE-754 64位二进制格式的字节,或
    • 使用调试器执行上述两种操作。

    此外,我建议通过将值101324.65625注入rstr[8](通过赋值或调试器)并以与上面使用的相同方式显示来测试是否足够好地显示浮点值。

    要测试2,仔细在分配后立即检查p1的值。我建议将上述内容应用于p1,而不是rstr[8]

    问题中显示的反汇编代码似乎反驳3.但是,我会考虑这些测试:

    • 测试这些指令是否实际执行,可能是通过在调试器中设置它们的断点。
    • 在执行之前立即检查调试器中的指令。
    • 检查要加载的内存,加载指令后的浮点寄存器以及存储后的内存。

答案 2 :(得分:1)

你需要做的(调试明智的)是在旧的和重构的版本之间获得rstr [20]和rstr [8]的二进制值。 tt和p1的二进制值也不会受到影响。这将证明阵列初始化相同。将double赋值给float数组,然后将其转换回double,并不是无损。

我能想到的唯一奇怪的情况是FPU的舍入模式在旧程序和重构程序之间设置不同。检查源代码“_control_fp(”,“fesetround(”或“fenv.h”。

答案 3 :(得分:-2)

浮点的第一个规则是结果是近似值,不应该假设是精确的。

编译器和CPU都能够进行大量优化,优化中的微小差异(包括缺乏操作)可能导致产生的“近似”的微小差异。这包括各种各样的事情,例如执行操作的顺序(例如,不要假设“(x + y)+ z”与“x +(y + z)”相同),如果有任何事情是由编译器完成(例如,常量折叠),如果内联或不内联,等等。

例如,(内部)80x86使用80位“扩展精度”浮点,它比双精度更精确;所以简单地将结果存储为double并再次加载会导致不同的结果重新使用FPU寄存器中已有的(更高精度)值。

我所说的大部分内容是,如果您获得的确切价值非常重要,那么您根本不应该使用浮点数(考虑“大理性”或其他内容)。