C++:浮点数、精度、虚拟机和GCC

21

我有以下代码:

#include <cstdio>
int main()
{
   if ((1.0 + 0.1) != (1.0 + 0.1))
      printf("not equal\n");
    else
      printf("equal\n");
    return 0;
}

使用gcc(4.4、4.5和4.6)的O3编译并在本机(ubuntu 10.10)上运行时,它会输出预期的“equal”结果。

然而,当以上述方式编译相同的代码并在虚拟机(ubuntu 10.10,virtualbox镜像)上运行时,它输出“not equal”--这是当设置了O3和O2标志但未设置O1及以下标志时的情况。当使用clang(O3和O2)编译并在虚拟机上运行时,我得到了正确的结果。

我知道1.1不能用double正确地表示,并阅读了“计算机科学家应该了解的关于浮点运算的一切”,所以请不要把我指向那里,这似乎是GCC进行的某种优化,在虚拟机中不能正常工作。

有什么想法吗?

注意:C++标准规定,此类情况下的类型提升是取决于具体实现的,可能是GCC使用了更精确的内部表示,因此在应用不等式测试时保持为真--由于额外的精度?

更新1:以上代码的以下修改现在导致正确的结果。似乎在某个时候,由于某种原因,GCC关闭了浮点控制字。

#include <cstdio>
void set_dpfpu() { unsigned int mode = 0x27F; asm ("fldcw %0" : : "m" (*&mode)); 
int main()
{
   set_dpfpu();
   if ((1.0 + 0.1) != (1.0 + 0.1))
      printf("not equal\n");
    else
      printf("equal\n");
    return 0;
}

更新2: 对于那些询问代码是否为const表达式的人,我已经将其更改如下,但在使用GCC编译时仍然失败。- 但我认为优化器可能会将以下内容转换为const表达式。

#include <cstdio>
void set_dpfpu() { unsigned int mode = 0x27F; asm ("fldcw %0" : : "m" (*&mode)); 
int main()
{
   //set_dpfpu();  uncomment to make it work.
   double d1 = 1.0;
   double d2 = 1.0;  
   if ((d1 + 0.1) != (d2 + 0.1))
      printf("not equal\n");
    else
      printf("equal\n");
    return 0;
}

更新3解决方案:将VirtualBox升级到版本4.1.8r75467可解决该问题。但是仍然存在一个问题,那就是:为什么clang版本能够工作。


4
请看编译器的输出。它生成了哪些代码? - David Heffernan
Virtualbox模拟了一个虚拟CPU,因此您不能依赖它使用相同的指令。 - tstenner
3
这个问题很难回答,因为它发生在一个非常特定的环境中。最好的方法是将你的代码拆开并检查它在做什么,这样你就能理解发生了什么。 - Renan Greinert
3
好问题。这两台机器上的gcc版本是相同的吗?如果在一台机器上编译,然后将生成的应用程序复制到另一台机器上,输出结果是否有差异? - Mr Lister
3
@Mr Lister:两台机器的gcc版本相同。我曾尝试复制在本地机器上构建的二进制文件,但结果错误相同。 - Jared Krumsie
显示剩余10条评论
4个回答

10
更新: 请参考此帖子 如何处理浮点运算中的超额精度? 它涉及扩展浮点精度的问题。我忘记了x86中的扩展精度。我记得有一个模拟应该是确定性的,但在Intel CPU和PowePC CPU上给出了不同的结果。原因是Intel的扩展精度架构。
这个网页讲述了如何将Intel CPU转换为双精度舍入模式: http://www.network-theory.co.uk/docs/gccintro/gccintro_70.html
虚拟机能否保证其浮点运算与硬件的浮点运算相同?我无法在快速的谷歌搜索中找到类似的保证。我也没有找到承诺 vituralbox FP 运算符合 IEEE 754的承诺。
虚拟机是模拟器,试图并且大多数情况下成功地模拟特定的指令集或体系结构。然而,它们只是模拟器,受其自身实现怪癖或设计问题的影响。
如果您还没有,请在 forums.virtualbox.org 上发布问题,并查看社区对此的看法。

问题在于使用clang时一切正常,而clang使用与gcc相同的链接器后端。 - Jared Krumsie
@JaredKrumsie:我仍然对VB社区的看法感兴趣。 - Alexandre C.
@JaredKrumsie:“同一个链接器” != “同一个编译器”。即使两个编译器都使用相同的链接器,它们也可能为计算生成不同的代码。在您的示例中,所有算术运算都将位于同一个*.o/*.obj文件中,因此“链接器”在这里并不重要。反汇编已编译的程序并查看实际发生的情况。或者阅读编译器的ASM输出。 - SigTerm
1
@Jared。我对LLVM了解不多。我进行了一次快速的谷歌搜索,看起来LLVM实现了其浮点数学。那么LLVM fp操作在不同的CPU或CPU模拟器上具有一致的行为是有意义的。因此,如果代码没有通过LLVM运行,则行为仍可能因CPU / VM而异。 - ahoffer

5

这种行为确实很奇怪,但其实很容易解释:

x86浮点寄存器在内部使用更高精度(例如80而不是64)。这意味着计算1.0 + 0.1将使用更高的精度进行计算(并且由于1.1根本无法在二进制中准确表示,那些额外的位将被使用)在寄存器中。只有将结果存储到内存中时才会被截断。

这意味着简单:如果您将从内存加载的值与新计算的寄存器中的值进行比较,则会返回“非相等”,因为一个值被截断而另一个值没有。因此,这与VM / no VM无关,它只取决于编译器生成的代码,这很容易波动,正如我们在那里看到的那样。

将其添加到日益增长的浮点惊喜列表中..


3
我没有提出这个答案是因为我期望当文本被编译成代码时,0.1会被截断。在高精度下进行计算除非结果比输入需要更多的位数,否则不应该有太大影响。现在想想,加法可能需要额外的一两个位,所以这是可能的。 - Mark Ransom
@Mark 确实,通常你会期望在执行某些生成新精度的操作(如sqrt、超越函数等)时出现这个问题,但这是我能想到的最好的解释了。也许在从内存加载某些内容时,扩展精度位没有被清除?或者我们可以加载80位的立即数?我不知道。恐怕有人需要查看英特尔手册。 - Voo

4
我可以确认你的非虚拟机代码存在同样的问题,但由于我没有虚拟机,所以我没有测试过虚拟机部分。
然而,编译器(包括Clang和GCC)将在编译时评估常量表达式。请参见以下汇编输出(使用gcc -O0 test.cpp -S):
    .file   "test.cpp"
    .section        .rodata
.LC0:
    .string "equal"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $.LC0, %edi
    call    puts
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1"
        .section        .note.GNU-stack,"",@progbits

看起来你理解汇编语言,但很明显只有"equal"字符串,没有"not equal"。因此比较甚至不会在运行时进行,它只会打印"equal"。

我建议你使用汇编语言编写计算和比较代码,并查看是否具有相同的行为。如果在虚拟机上有不同的行为,则是虚拟机执行计算的方式不同。

更新1:(基于原问题中的“更新2”)。以下是gcc -O0 -S test.cpp输出的汇编代码(适用于64位架构)。你可以看到movabsq $4607182418800017408, %rax这一行出现了两次。这将是两个比较标志,我还没有验证,但我认为$4607182418800017408值在浮点术语中是1.1。如果在虚拟机上编译这个代码,如果得到相同的结果(两个类似的行),那么虚拟机在运行时会做一些有趣的事情,否则就是虚拟机和编译器的组合。

main:
.LFB1:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movabsq $4607182418800017408, %rax
        movq    %rax, -16(%rbp)
        movabsq $4607182418800017408, %rax
        movq    %rax, -8(%rbp)
        movsd   -16(%rbp), %xmm1
        movsd   .LC1(%rip), %xmm0
        addsd   %xmm1, %xmm0
        movsd   -8(%rbp), %xmm2
        movsd   .LC1(%rip), %xmm1
            addsd   %xmm2, %xmm1
        ucomisd %xmm1, %xmm0
        jp      .L6
        ucomisd %xmm1, %xmm0
        je      .L7

2
我看到你添加了另一个问题:
注意:C++标准规定在这种情况下类型提升是实现相关的,难道GCC使用更精确的内部表示方式,当应用不等式测试时会保持正确-由于额外的精度?
答案是否定的。无论格式有多少位,1.1都不能在二进制格式中准确地表示。你可以接近,但不能使用无数个零来表示.1之后的数。
或者你是指十进制的全新内部格式?不,我不相信那样做。如果是这样的话,它将不太兼容。

4
如果同样的计算被执行两次,有可能会将一个结果存储在内存中(即64位),而另一个结果保留在浮点寄存器中(即80位)。在我看来,这应该算作两种不同的格式。 - Mark Ransom
是的,但你无法在FP 80位格式中精确表示1.1。那就是问题所在。 - Mr Lister

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