从 float 转换为 double 会产生不同的结果——相同的代码、相同的编译器、相同的操作系统

Posted

技术标签:

【中文标题】从 float 转换为 double 会产生不同的结果——相同的代码、相同的编译器、相同的操作系统【英文标题】:Cast from float to double produces different results—same code, same compiler, same OS 【发布时间】:2013-11-21 15:55:07 【问题描述】:

编辑:请参阅问题的结尾以获取最新的答案。

我花了数周时间在我的一个软件中寻找一个非常奇怪的错误 维持。长话短说,有一个旧软件在 分发,以及需要匹配输出的新软件 老的。两者(理论上)依赖于一个公共库。 [1]然而,我不能 复制由库的原始版本生成的结果, 即使库的两个版本的源匹配。实际上 有问题的代码非常简单。原始版本看起来像这样( “voodoo”评论不是我的):[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) 产生相同的结果。 然而p1101325.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 Postpischil 和unwind 提示我再次检查并及时反馈。这非常很有帮助。


脚注

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

【问题讨论】:

请注意,文字 373.15 是双精度数。您将其分配给浮点数。浮点数无法准确表示 373.15 。但是,如果来源相同,那有什么不同呢?不同的编译器?编译器设置?对 fesetround() 的不同调用,使用非标准 _control_fp() ? 正确,这就是为什么我对程序的原始行为并不感到惊讶,其中转换为 double 后显示的值是长小数而不是精确的。 注意:一般来说,典型的double 需要 17 个十进制数字来唯一地表示其值。 101324.65625 显示 11 和 373.149999999996 显示 15。精确度可能不是这里的问题,但可能有帮助。此外 - 我怀疑您确定 rstr[8] 具有值 101325 是由于具有整数截断的 rstr[8] 的整数视图,它实际上具有 101325 21/32 的 精确 值。跨度> 显示(从两个代码中)rstr[20] 的十六进制值和此分配后的 tttt = (double) rstr[20] - 与 rstr[8]p1 var 相同。恕我直言 - 你的调试方法有问题,或者你误解了你看到的东西。 @chux,如果有整数截断,它在调试器视图中。这当然是可能的,但它报告的不是101325,而是101325.——注意小数点。它可能不准确地将其报告为整数小数,如果是这样,那不是我在 VC6 中发现的第一个错误,但它可能不是截断。 【参考方案1】:

这个:

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

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

【讨论】:

好主意,但不,我已经检查过了;它实际上是准确的。更重要的是,在除了原始程序(包括程序的重构版本,无论是在 VC6、GCC 还是 clang 中编译)之外的任何上下文中,它最终都以 101325. 的形式分配给双重 101325. — 在 VC6 中和 GCC。 如前所述,我实际上将再次提取十六进制值以确认它们是相同的。在我看来,主要的好奇点是,当使用相同的编译器编译时,它的行为会有所不同。 我现在可耻地收回我之前的声明。 :/ 我以为我检查过这些是相同的,但我记得我检查过的一组相关值。我会相应地更新问题。【参考方案2】:

可能性是:

    尽管报告了观察结果,rstr[8] 在分配给p1 之前的原始程序中的值是 101324.65625,而不是报告的 101325。 尽管报告了观察结果,p1 在分配后立即没有值 101324.65625。 程序未正确执行分配(包括转换为double)。

要测试 1,请在分配前仔细检查 rstr[8] 的值。我建议:

将值打印或记录为 20 位有效数字,并且 打印或记录构成 rstr[8] 的字节,然后以 IEEE-754 64 位二进制格式解释字节,或 使用调试器完成上述两项工作。

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

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

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

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

【讨论】:

很好的建议,谢谢。正如我对上述原始问题的评论中所述,我将继续提取字节并检查它们。我应该注意到,最让我困惑的一点不是原始分配——这并不让我感到惊讶——而是它在同一个库的两种不同方法之间的行为不同。 是时候吃我的话了!请参阅我在上面添加的更新。感谢您的反馈和有关查看数据的最佳方式的详细说明。事实证明,选项 1 就是这种情况。 @ChrisKrycho:很高兴有一个涉及确定行为的错误,可以分成案例并进行测试。【参考方案3】:

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

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

【讨论】:

101324.65625 和 101325 都可以在 float 中精确表示(在 OP 使用的实现中,IEEE-754 32 位二进制浮点)。无论采用何种舍入模式,从doublefloat 的转换都不会出现任何错误或舍入。【参考方案4】:

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

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

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

我的主要意思是,如果你得到的确切值如此重要,那么你根本就不应该使用浮点数(考虑“大理性”之类的东西)。

【讨论】:

您需要更仔细地阅读问题。这是一项维护任务;我不会选择或提倡这种方法(实际上不在未来的发展中),但肯定不可能简单地撤销历史。 :)

以上是关于从 float 转换为 double 会产生不同的结果——相同的代码、相同的编译器、相同的操作系统的主要内容,如果未能解决你的问题,请参考以下文章

如何将 Python Decimal 实例转换为 C++ double?

JAVA :int 转换为bite 以及double 转换为float 视频里解释成字节数不同又是咔嚓砍掉的是啥意思?请高...

为啥 int 被提升为 double 而不是 float 以进行隐式转换

错误83错误C2398:从'double'转换为'float'需要缩小转换

为啥在 gcc 和 clang 上通过优化将大 double 转换为 uint16_t 会给出不同的答案

c语言 不同数据类型间的混合运算