ARM和INTEL平台之间的g ++数学差异

时间:2014-10-02 21:25:29

标签: math gcc g++

以下程序在INTEL平台上提供8191。在ARM平台上,它给出了8192(正确的答案)。

// g++ -o test test.cpp
#include <stdio.h>

int main( int argc, char *argv[])
{
    double a = 8192.0 / (4 * 510);
    long x = (long) (a * (4 * 510));
    printf("%ld\n", x);
    return 0;
}

任何人都可以解释原因吗?如果我使用任何-O,-O2或-O3编译开关,问题就会消失。

提前致谢!

2 个答案:

答案 0 :(得分:0)

long fun ( void )
{
    double a = 8192.0 / (4 * 510);
    long x = (long) (a * (4 * 510));
    return(x);
}

g ++ -c -O2 fun.c -o fun.o objdump -D fun.o

0000000000000000 <_Z3funv>:
   0:   b8 00 20 00 00          mov    $0x2000,%eax
   5:   c3                      retq   

没有数学,编译器完成了所有数学操作,删除了你提供的所有死代码。

gcc同样的交易。

0000000000000000 <fun>:
   0:   b8 00 20 00 00          mov    $0x2000,%eax
   5:   c3                      retq   

手臂gcc优化

00000000 <fun>:
   0:   e3a00a02    mov r0, #8192   ; 0x2000
   4:   e12fff1e    bx  lr

双a的原始二进制是

0x40101010 0x10101010

和double(4 * 510)

0x409FE000 0x00000000

那些是在编译时完成的计算,即使没有优化。

通用软浮臂

00000000 <fun>:
   0:   e92d4810    push    {r4, fp, lr}
   4:   e28db008    add fp, sp, #8
   8:   e24dd014    sub sp, sp, #20
   c:   e28f404c    add r4, pc, #76 ; 0x4c
  10:   e8940018    ldm r4, {r3, r4}
  14:   e50b3014    str r3, [fp, #-20]
  18:   e50b4010    str r4, [fp, #-16]
  1c:   e24b1014    sub r1, fp, #20
  20:   e8910003    ldm r1, {r0, r1}
  24:   e3a02000    mov r2, #0
  28:   e59f3038    ldr r3, [pc, #56]   ; 68 <fun+0x68>
  2c:   ebfffffe    bl  0 <__aeabi_dmul>
  30:   e1a03000    mov r3, r0
  34:   e1a04001    mov r4, r1
  38:   e1a00003    mov r0, r3
  3c:   e1a01004    mov r1, r4
  40:   ebfffffe    bl  0 <__aeabi_d2iz>
  44:   e1a03000    mov r3, r0
  48:   e50b3018    str r3, [fp, #-24]
  4c:   e51b3018    ldr r3, [fp, #-24]
  50:   e1a00003    mov r0, r3
  54:   e24bd008    sub sp, fp, #8
  58:   e8bd4810    pop {r4, fp, lr}
  5c:   e12fff1e    bx  lr
  60:   10101010    andsne  r1, r0, r0, lsl r0
  64:   40101010    andsmi  r1, r0, r0, lsl r0
  68:   409fe000    addsmi  lr, pc, r0
  6c:   e1a00000    nop         ; (mov r0,

英特尔

  0000000000000000 <fun>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 b8 10 10 10 10 10    movabs $0x4010101010101010,%rax
   b:   10 10 40 
   e:   48 89 45 f0             mov    %rax,-0x10(%rbp)
  12:   f2 0f 10 4d f0          movsd  -0x10(%rbp),%xmm1
  17:   f2 0f 10 05 00 00 00    movsd  0x0(%rip),%xmm0        # 1f <fun+0x1f>
  1e:   00 
  1f:   f2 0f 59 c1             mulsd  %xmm1,%xmm0
  23:   f2 48 0f 2c c0          cvttsd2si %xmm0,%rax
  28:   48 89 45 f8             mov    %rax,-0x8(%rbp)
  2c:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  30:   5d                      pop    %rbp
  31:   c3                      retq   


0000000000000000 <.rodata>:
   0:   00 00                   add    %al,(%rax)
   2:   00 00                   add    %al,(%rax)
   4:   00 e0                   add    %ah,%al
   6:   9f                      lahf   
   7:   40                      rex

所以你可以在手臂中看到它正在取4.whatever(一个值)而4 * 510转换为double值并将它们传递给aeabi_dmul(双倍乘以毫无疑问)。然后它将它从double转换为整数(d2i)然后你去。

英特尔同样的交易,但硬浮动指令。

因此,如果存在差异(我必须准备并启动一个手臂以查看至少一个手臂结果,已经发布我的英特尔结果,您的程序逐字记录是8192)将在两个浮动中的一个点操作(乘以或加倍或整数)和舍入选项可能起作用。

这显然不是一个可以在基数二(浮点)中清晰表示的值。

0x40101010 0x10101010

开始用数学计算,其中一个可能会导致四舍五入的差异。

最后也是最重要的,只是因为奔腾虫很有名,而且我们认为它们已经固定了它,浮点单元仍然存在错误...但通常程序员在此之前陷入浮点精度陷阱如果你真的在这里看到任何东西,你可能会看到这里......

答案 1 :(得分:0)

如果使用优化标记,问题消失的原因是因为此结果已知 a priori ,即编译器只能替换x语句中的printf 8192并节省内存。事实上,我愿意赌钱,因为编译器负责你观察到的差异。

这个问题基本上是“how do computers store numbers”,这个问题总是与C ++(或C)中的编程有关。我建议您在阅读之前先查看链接。

让我们看看这两行:

double a = 8192.0 / (4 * 510);
long x = (long) (a * (4 * 510));

首先,请注意您将两个int个常数相乘 - 隐式地,4 * 510(int)(4 * 150)相同。但是,C(和C ++)有一个快乐的规则,当人们写a/3之类的东西时,计算是用浮点运算而不是整数运算完成的。计算以double完成,除非两个操作数都是float,在这种情况下,计算是在float中完成的,如果两个操作数都是int s则是整数。我怀疑您可能会遇到ARM目标的精度问题。我们确定一下。

为了我自己的好奇心,我通过调用gcc -c -g -Wa,-a,-ad test.c > test.s将你的程序编译成程序集。在两个不同版本的GCC上,它们都是类Unix操作系统,这个代码片段总是弹出8192而不是8191.

这种特殊的标志组合包括相应的C行作为汇编注释,这使得很多更容易阅读正在发生的事情。这是有趣的位,用AT&T Syntax,编写,即命令的格式为command source, destination

 30    5:testSpeed.c   ****     double a = 8192.0 / (4 * 510);
 31   23                    .loc 1 5 0
 32   24 000f 48B81010      movabsq $4616207279229767696, %rax
 33   24      10101010
 34   24      1040
 35   25 0019 488945F0      movq    %rax, -16(%rbp)

糟糕!让我们稍微分解一下。行3036处理将四字节值4616207279229767696分配给寄存器rax,寄存器寄存器保存值。下一行 - movq %rax, -16(%rbp) - 将该数据移动到rbp指向的内存中的位置。

因此,换句话说,编译器已经分配了一个内存而忘记了它。

下一组线条有点复杂。

 36    6:testSpeed.c   ****     long x = (long) (a * (4 * 510));
 37   26                    .loc 1 6 0
 38   27 001d F20F104D      movsd   -16(%rbp), %xmm1
 39   27      F0
 40   28 0022 F20F1005      movsd   .LC1(%rip), %xmm0
 41   28      00000000
 42   29 002a F20F59C1      mulsd   %xmm1, %xmm0
 43   30 002e F2480F2C      cvttsd2siq  %xmm0, %rax
 44   30      C0
 45   31 0033 488945F8      movq    %rax, -8(%rbp)
 ...
 72   49                .LC1:
 73   50 0008 00000000      .long   0
 74   51 000c 00E09F40      .long   1084219392
 75   52                    .text
 76   53                .Letext0:

这里,我们首先将上面指向的寄存器的内容 - 即a - 移动到寄存器(xmm1)。然后我们将下面显示的snipped中指向的数据,.LC1,并将其卡入另一个寄存器(xmm0)。令我惊讶的是,我们做了scalar floating-point double precision multiplymulsd)。然后,我们通过调用long截断结果(这是您向[{1}}实际执行的操作,并将结果放在某处(cvttsd2siq)。

movq    %rax, -8(%rbp)

此代码的其余部分只调用 46 7:testSpeed.c **** printf("%ld\n", x); 47 32 .loc 1 7 0 48 33 0037 488B45F8 movq -8(%rbp), %rax 49 34 003b 4889C6 movq %rax, %rsi 50 35 003e BF000000 movl $.LC2, %edi 51 35 00 52 36 0043 B8000000 movl $0, %eax 53 36 00 54 37 0048 E8000000 call printf 55 37 00 56 8:testSpeed.c **** return 0;

现在,让我们再次做同样的事情,但使用printf,即告诉编译器进行相当积极的优化。以下是生成的程序集中的一些选择代码段:

-O3

在这种情况下,我们看到编译器甚至不打算从您的代码生成指令,而只是简单地勾勒出正确的答案。

为了论证,我对139 22 .loc 2 104 0 140 23 0004 BA002000 movl $8192, %edx ... 154 5:testSpeed.c **** double a = 8192.0 / (4 * 510); 155 6:testSpeed.c **** long x = (long) (a * (4 * 510)); 156 7:testSpeed.c **** printf("%ld\n", x); 157 8:testSpeed.c **** return 0; 158 9:testSpeed.c **** } ... 做了同样的事情。装配是相同的。

你的ARM目标会发生类似的情况,但浮点乘法是不同的,因为它是一个不同的处理器(以上所有内容仅适用于x86_64)。 记住,如果你看到C(或C ++)编程时你没想到的东西,你需要停下来思考它。像0.3这样的分数无法在内存中有限地表示!