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.98
和 q = 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 位版本中所做的那样。
我在不同的 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.98
和 q = 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 位版本中所做的那样。