【问题标题】:C++: doubles, precision, virtual machines and GCCC++:双打、精度、虚拟机和 GCC
【发布时间】:2015-06-14 17:46:02
【问题描述】:

我有以下代码:

#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)时,它会打印“等于”的预期结果。

但是,当按照上述方式编译并在虚拟机(ubuntu 10.10,virtualbox 映像)上运行相同的代码时,它会输出“不等于” - 这是设置了 O3 和 O2 标志但未设置 O1 和以下。当使用 clang(O3 和 O2)编译并在虚拟机上运行时,我得到了正确的结果。

我了解 1.1 无法使用 double 正确表示,并且我已阅读 “每个计算机科学家应该了解的浮点运算知识” 所以请不要指点我,这似乎是 GCC 所做的某种优化,但它似乎在虚拟机中不起作用。

有什么想法吗?

注意:C++ 标准说在这种情况下的类型提升是依赖于实现的,难道 GCC 使用了更精确的内部表示,当应用不等式测试时它仍然成立 - 由于额外的精度?

UPDATE1: 对上述代码进行以下修改,现在可以得到正确的结果。似乎在某些时候,无论出于何种原因,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;
}

UPDATE2: 对于那些询问代码的 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;
}

UPDATE3 解决方案:将 virtualbox 升级到版本 4.1.8r75467 解决了该问题。然而,他们仍然存在一个问题,那就是:为什么 clang 构建工作。

【问题讨论】:

  • 看看编译器的输出。它产生什么代码?
  • 在这两种情况下你使用相同版本的 GCC 吗?
  • 这个问题很难回答,因为它发生在一个非常具体的环境中。最好的方法是反汇编你的代码并检查它在做什么。
  • 好问题。两台机器上的gcc版本是一样的吗?如果你在一台机器上编译然后将生成的应用程序复制到另一台机器上会怎样?那么输出有区别吗?
  • @Mr Lister:两台机器的 gcc 版本是一样的。我尝试在本机机器上构建的二进制文件中进行复制,但我得到了同样的错误结果。

标签: c++ optimization compiler-construction double-precision vm-implementation


【解决方案1】:

更新:请参阅此帖子How to deal with excess precision in floating-point computations? 它解决了扩展浮点精度的问题。我忘记了 x86 中的扩展精度。我记得有一个模拟应该是确定性的,但在 Intel CPU 上给出的结果与在 PowePC CPU 上的结果不同。原因是英特尔的扩展精度架构。

此网页讨论如何将 Intel CPU 置于双精度舍入模式:http://www.network-theory.co.uk/docs/gccintro/gccintro_70.html


virtualbox 是否保证其浮点运算与硬件的浮点运算相同?通过快速谷歌搜索,我找不到这样的保证。我也没有找到 vituralbox FP ops 符合 IEEE 754 的承诺。

VM 是尝试(并且大部分成功)模拟特定指令集或架构的模拟器。然而,它们只是模拟器,并受制于自己的实现怪癖或设计问题。

如果您还没有,请发布问题 forums.virtualbox.org 并查看社区对此的看法。

【讨论】:

  • 问题是使用 clang 可以正常工作 - 并且 clang 使用了与 gcc 相同的链接器 bank-end。
  • @JaredKrumsie:我仍然对 VB 社区对此的看法很感兴趣。
  • @JaredKrumsie:“相同的链接器”!=“相同的编译器”。不同的编译器可以为计算生成不同的代码,即使它们都使用相同的链接器。您示例中的所有算术都将位于同一个 .o/.obj 文件中,因此“链接器”在这里并不重要。反汇编你的编译程序,看看到底发生了什么。或者读取编译器的 ASM 输出。
  • @Jared。我对 LLVM 了解不多。 A 做了一个快速的谷歌搜索,看起来 LLVM 实现了它的浮点数学。因此,LLVM fp ops 在不同的 CPU 或 CPU 仿真器上具有一致的行为是有道理的。因此,如果代码没有通过 LLVM 运行,那么行为仍可能因 CPU/VM 而异。
【解决方案2】:

是的,这确实是一种奇怪的行为,但实际上可以很容易地解释:

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

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

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

【讨论】:

  • 我没有提出这个答案,因为我希望 0.1 在编译时在将文字发送到代码时被截断。除非结果需要比输入更多的位,否则以更高的精度进行数学运算并不重要。经过反思,添加可能需要一两个额外的位,所以我认为这是可能的。
  • @Mark 确实如此,通常你会在进行一些确实产生新精度的操作(sqrt,超越函数,..)时期望这个问题,但这是我能想到的最好的。从内存中加载某些内容时,可能没有清除扩展精度位?或者我们可以加载 80 位立即数?不知道。我担心有人必须查看英特尔手册。
【解决方案3】:

我可以确认您的非 VM 代码的相同行为,但由于我没有 VM,因此我没有测试 VM 部分。

但是,编译器,无论是 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

看起来你懂汇编,但很明显只有“相等”字符串,没有“不相等”。所以比较甚至没有在运行时完成,它只是打印“相等”。

我会尝试使用汇编对计算和比较进行编码,看看你是否有相同的行为。如果您在 VM 上有不同的行为,那么这就是 VM 进行计算的方式。

更新 1:(基于原始问题中的“更新 2”)。下面是gcc -O0 -S test.cpp 输出程序集(用于 64 位架构)。在其中你可以看到movabsq $4607182418800017408, %rax 行两次。这将用于两个比较标志,我尚未验证,但我假设 $4607182418800017408 的浮点值是 1.1。在 VM 上编译它会很有趣,如果你得到相同的结果(两条相似的行),那么 VM 将在运行时做一些有趣的事情,否则它是 VM 和编译器的组合。

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

【讨论】:

    【解决方案4】:

    我看到你添加了另一个问题:

    注意:C++ 标准说这种情况下的类型提升是依赖于实现的,难道 GCC 使用了更精确的内部表示,当应用不等式测试时,由于额外的精度,它仍然成立?

    答案是否定的。 1.1 不能以二进制格式精确表示,无论格式有多少位。您可以接近,但不能在.1 后面加上无数个零。

    或者您的意思是一种全新的小数内部格式?不,我拒绝相信。如果是这样,那将不是很兼容。

    【讨论】:

    • 如果执行两次相同的计算,很可能一个结果存储在内存中(即 64 位),一个结果保留在浮点寄存器中(即 80 位)。在我的书中,这将算作两种不同的格式。
    • 是的,但是您不能以 FP 80 位格式精确表达 1.1。这就是问题所在。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-18
    • 1970-01-01
    • 1970-01-01
    • 2010-11-11
    • 1970-01-01
    • 1970-01-01
    • 2011-02-11
    相关资源
    最近更新 更多