gcc '-m32' 选项在不是 运行 valgrind 时更改浮点舍入

gcc '-m32' option changes floating-point rounding when not running valgrind

我在不同的 build/execute 场景下得到不同的浮点舍入。注意下面第二个 运行 中的 2498...

   #include <iostream>
   #include <cassert>
   #include <typeinfo>
   using std::cerr;

void domath( int n, double c, double & q1, double & q2 )
   {
   q1=n*c;
   q2=int(n*c);
   }

int main()
   {
   int n=2550;
   double c=0.98, q1, q2;
   domath( n, c, q1, q2 );
   cerr<<"sizeof(int)="<<sizeof(int)<<", sizeof(double)="<<sizeof(double)<<", sizeof(n*c)="<<sizeof(n*c)<<"\n";
   cerr<<"n="<<n<<", int(q1)="<<int(q1)<<", int(q2)="<<int(q2)<<"\n";
   assert( typeid(q1) == typeid(n*c) );
   }

运行 作为 64 位可执行文件...

$ g++ -m64 -Wall rounding_test.cpp -o rounding_test && ./rounding_test
sizeof(int)=4, sizeof(double)=8, sizeof(n*c)=8
n=2550, int(q1)=2499, int(q2)=2499

运行 作为 32 位可执行文件...

$ g++ -m32 -Wall rounding_test.cpp -o rounding_test && ./rounding_test
sizeof(int)=4, sizeof(double)=8, sizeof(n*c)=8
n=2550, int(q1)=2499, int(q2)=2498

运行 作为 valgrind 下的 32 位可执行文件...

$ g++ -m32 -Wall rounding_test.cpp -o rounding_test && valgrind --quiet ./rounding_test
sizeof(int)=4, sizeof(double)=8, sizeof(n*c)=8
n=2550, int(q1)=2499, int(q2)=2499

为什么我在使用 -m32 编译时看到不同的结果,为什么在 运行ning valgrind 时结果又不同?

我的系统是Ubuntu 14.04.1 LTS x86_64,我的gcc是4.8.2版本。


编辑:

应反汇编的要求,我对代码进行了一些重构,以便将相关部分隔离开来。 -m64-m32 之间采用的方法显然有很大不同。我不太关心为什么这些会给出不同的舍入结果,因为我可以通过应用 round() 函数来解决这个问题。最有趣的问题是:为什么valgrind会改变结果?

rounding_test:     file format elf64-x86-64 
                                  <
000000000040090d <_Z6domathidRdS_>:               <
  40090d:   55                      push   %rbp       <
  40090e:   48 89 e5                mov    %rsp,%rbp      <
  400911:   89 7d fc                mov    %edi,-0x4(%rbp <
  400914:   f2 0f 11 45 f0          movsd  %xmm0,-0x10(%r <
  400919:   48 89 75 e8             mov    %rsi,-0x18(%rb <
  40091d:   48 89 55 e0             mov    %rdx,-0x20(%rb <
  400921:   f2 0f 2a 45 fc          cvtsi2sdl -0x4(%rbp), <
  400926:   f2 0f 59 45 f0          mulsd  -0x10(%rbp),%x <
  40092b:   48 8b 45 e8             mov    -0x18(%rbp),%r <
  40092f:   f2 0f 11 00             movsd  %xmm0,(%rax)   <
  400933:   f2 0f 2a 45 fc          cvtsi2sdl -0x4(%rbp), <
  400938:   f2 0f 59 45 f0          mulsd  -0x10(%rbp),%x <
  40093d:   f2 0f 2c c0             cvttsd2si %xmm0,%eax  <
  400941:   f2 0f 2a c0             cvtsi2sd %eax,%xmm0   <
  400945:   48 8b 45 e0             mov    -0x20(%rbp),%r <
  400949:   f2 0f 11 00             movsd  %xmm0,(%rax)   <
  40094d:   5d                      pop    %rbp       <
  40094e:   c3                      retq              <

      | rounding_test:     file format elf32-i386

                                  > 0804871d <_Z6domathidRdS_>:
                                  >  804871d:   55                      push   %ebp
                                  >  804871e:   89 e5                   mov    %esp,%ebp
                                  >  8048720:   83 ec 10                sub    [=15=]x10,%esp
                                  >  8048723:   8b 45 0c                mov    0xc(%ebp),%eax
                                  >  8048726:   89 45 f8                mov    %eax,-0x8(%ebp
                                  >  8048729:   8b 45 10                mov    0x10(%ebp),%ea
                                  >  804872c:   89 45 fc                mov    %eax,-0x4(%ebp
                                  >  804872f:   db 45 08                fildl  0x8(%ebp)
                                  >  8048732:   dc 4d f8                fmull  -0x8(%ebp)
                                  >  8048735:   8b 45 14                mov    0x14(%ebp),%ea
                                  >  8048738:   dd 18                   fstpl  (%eax)
                                  >  804873a:   db 45 08                fildl  0x8(%ebp)
                                  >  804873d:   dc 4d f8                fmull  -0x8(%ebp)
                                  >  8048740:   d9 7d f6                fnstcw -0xa(%ebp)
                                  >  8048743:   0f b7 45 f6             movzwl -0xa(%ebp),%ea
                                  >  8048747:   b4 0c                   mov    [=15=]xc,%ah
                                  >  8048749:   66 89 45 f4             mov    %ax,-0xc(%ebp)
                                  >  804874d:   d9 6d f4                fldcw  -0xc(%ebp)
                                  >  8048750:   db 5d f0                fistpl -0x10(%ebp)
                                  >  8048753:   d9 6d f6                fldcw  -0xa(%ebp)
                                  >  8048756:   8b 45 f0                mov    -0x10(%ebp),%e
                                  >  8048759:   89 45 f0                mov    %eax,-0x10(%eb
                                  >  804875c:   db 45 f0                fildl  -0x10(%ebp)
                                  >  804875f:   8b 45 18                mov    0x18(%ebp),%ea
                                  >  8048762:   dd 18                   fstpl  (%eax)
                                  >  8048764:   c9                      leave  
                                  >  8048765:   c3                      ret    

编辑: 看来,至少很久以前,valgrind 的浮点计算不如 "real" 计算准确。换句话说,这可以解释为什么你会得到不同的结果。请参阅 valgrind 邮件列表上的 this 问答。

Edit2: 并且当前的 valgrind.org 文档在 "core limitations" 部分 here - 所以我希望它确实是"still valid"。换句话说,valgrind 的文档说预计 valgrind 和 x87 FPU 计算之间存在差异。 "You have been warned!"(正如我们所见,使用 sse 指令执行相同的数学运算会产生与 valgrind 相同的结果,确认这是 "rounding from 80 bits to 64 bits" 差异)

浮点计算将根据计算的具体执行方式略有不同。我不确定你到底想得到什么答案,所以这里有一个长篇大论 "answer of a sort".

Valgrind 确实以各种方式改变了程序的确切行为(它模拟某些指令,而不是实际执行真正的指令——这可能包括保存计算的中间结果)。此外,浮点计算对于 "not be precise" 来说是众所周知的 - 如果计算结果准确与否,这只是 luck/bad 运气的问题。 0.98 是许多无法用浮点格式精确描述的数字之一 [至少不是常见的 IEEE 格式]。

通过添加:

cerr<<"c="<<std::setprecision(30)<<c <<"\n";

我们看到输出是 c=0.979999999999999982236431605997(是的,实际值是 0.979999...99982 或类似的数字,剩余数字只是剩余值,因为它不是 "even" 二进制号,总会有剩余的。

这是由 gcc 生成的代码的 n = 2550;c = 0.98q = n * c 部分:

movl    50, -28(%ebp)       ; n
fldl    .LC0
fstpl   -40(%ebp)              ; c
fildl   -28(%ebp)
fmull   -40(%ebp)
fstpl   -48(%ebp)              ; q - note that this is stored as a rouned 64-bit value.

这是代码的 int(q)int(n*c) 部分:

fildl   -28(%ebp)             ; n
fmull   -40(%ebp)             ; c 
fnstcw  -58(%ebp)             ; Save control word
movzwl  -58(%ebp), %eax
movb    , %ah
movw    %ax, -60(%ebp)        ; Save float control word.
fldcw   -60(%ebp)
fistpl  -64(%ebp)             ; Store as integer (directly from 80-bit result)
fldcw   -58(%ebp)             ; restore float control word.
movl    -64(%ebp), %ebx       ; result of int(n * c)


fldl    -48(%ebp)             ; q
fldcw   -60(%ebp)             ; Load float control word as saved above.
fistpl  -64(%ebp)             ; Store as integer.
fldcw   -58(%ebp)             ; Restore control word.
movl    -64(%ebp), %esi       ; result of int(q)

现在,如果在这些计算之一的中间从内部 80 位精度存储(并因此四舍五入)中间结果,则结果可能与计算时未保存中间结果的结果略有不同值。

我从 g++ 4.9.2 和 clang++ -mno-sse 得到相同的结果 - 但如果我在 clang 情况下启用 sse,它会给出与 64 位构建相同的结果。使用 gcc -msse2 -m32 到处都会给出 2499 的答案。这表明错误是由 "storing intermediate results" 以某种方式引起的。

同样,在 gcc 中优化为 -O1 将在所有地方给出 2499 - 但这是巧合,而不是某些 "clever thinking" 的结果。如果您想要正确舍入计算的整数值,最好自己舍入,因为迟早 int(someDoubleValue) 会出现 "one short"。

Edit3: 最后,使用 g++ -mno-sse -m64 也会产生相同的 2498 答案,而在同一个二进制文件上使用 valgrind 会产生2499 答案。

32位版本使用X87 floating point instructions。 X87内部使用80位浮点数,在与其他精度相互转换时会出现问题。在您的情况下,0.98 的 64 位精度近似值略小于真实值。当 CPU 将其转换为 80 位值时,您会得到完全相同的数值,这是一个同样糟糕的近似值——拥有更多位并不能使您获得更好的近似值。然后 FPU 将该数字乘以 2550,得到一个略小于 2499 的数字。如果 CPU 一直使用 64 位数字,它应该精确计算 2499,就像它在 64 位版本中所做的那样。