我已经能够使用单个版本的编译器重现该问题。
我的是 MinGW g++ 4.6.2。
当我将程序编译为g++ -g -O2 bugflt.cpp -o bugflt.exe
时,我得到720720
.
这是反汇编main()
:
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
call ___main
movl $720720, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSolsEi
movl %eax, (%esp)
call __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
xorl %eax, %eax
leave
ret
如您所见,该值是在编译时计算的。
当我将它编译为 时g++ -g -O2 -fno-inline bugflt.cpp -o bugflt.exe
,我得到720719
.
这是反汇编main()
:
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
call ___main
movl $1, 4(%esp)
movl $13, (%esp)
call __ZSt3powIiiEN9__gnu_cxx11__promote_2INS0_11__enable_ifIXaasrSt15__is_arithmeticIT_E7__valuesrS3_IT0_E7__valueES4_E6__typeES6_E6__typeES4_S6_
fmuls LC1
fnstcw 30(%esp)
movw 30(%esp), %ax
movb $12, %ah
movw %ax, 28(%esp)
fldcw 28(%esp)
fistpl 4(%esp)
fldcw 30(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSolsEi
movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, 4(%esp)
movl %eax, (%esp)
call __ZNSolsEPFRSoS_E
xorl %eax, %eax
leave
ret
...
LC1:
.long 1196986368 // 55440.0 exactly
如果我将调用替换为exp()
像这样加载 13.0:
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
call ___main
movl $1, 4(%esp)
movl $13, (%esp)
// call __ZSt3powIiiEN9__gnu_cxx11__promote_2INS0_11__enable_ifIXaasrSt15__is_arithmeticIT_E7__valuesrS3_IT0_E7__valueES4_E6__typeES6_E6__typeES4_S6_
fildl (%esp)
fmuls LC1
fnstcw 30(%esp)
movw 30(%esp), %ax
movb $12, %ah
movw %ax, 28(%esp)
fldcw 28(%esp)
fistpl 4(%esp)
fldcw 30(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSolsEi
movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, 4(%esp)
movl %eax, (%esp)
call __ZNSolsEPFRSoS_E
xorl %eax, %eax
leave
ret
我明白了720720
。
exp()
如果我在如下fistpl 4(%esp)
指令的持续时间内设置 x87 FPU 控制字的相同舍入和精度控制字段:
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
call ___main
movl $1, 4(%esp)
movl $13, (%esp)
fnstcw 30(%esp)
movw 30(%esp), %ax
movb $12, %ah
movw %ax, 28(%esp)
fldcw 28(%esp)
call __ZSt3powIiiEN9__gnu_cxx11__promote_2INS0_11__enable_ifIXaasrSt15__is_arithmeticIT_E7__valuesrS3_IT0_E7__valueES4_E6__typeES6_E6__typeES4_S6_
fldcw 30(%esp)
fmuls LC1
fnstcw 30(%esp)
movw 30(%esp), %ax
movb $12, %ah
movw %ax, 28(%esp)
fldcw 28(%esp)
fistpl 4(%esp)
fldcw 30(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSolsEi
movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, 4(%esp)
movl %eax, (%esp)
call __ZNSolsEPFRSoS_E
xorl %eax, %eax
leave
ret
我也明白720720
。
由此我只能得出结论,exp()
计算 13 1不精确为 13.0。
可能值得查看它的源代码,__gnu_cxx::__promote_2<__gnu_cxx::__enable_if<(std::__is_arithmetic<int>::__value)&&(std::__is_arithmetic<int>::__value), int>::__type, int>::__type std::pow<int, int>(int, int)
以确切了解它如何设法用整数搞砸求幂(请参阅,与 C 不同,exp()
它需要两个ints
而不是两个doubles
)。
但我不会为此责备exp()
。C++11 定义float pow(float, float)
了 long double pow(long double, long double)
C 的double pow(double, double)
. 但double pow(int, int)
标准中没有。
编译器为整数参数提供版本这一事实并没有对结果的精度做出任何额外的保证。如果exp()
计算 a b为
a b = 2 b * log 2 (a)
或作为
a b = e b * ln(a)
对于浮点值,过程中肯定会存在舍入误差。
如果“整数”版本的exp()
做类似的事情并由于舍入误差而导致类似的精度损失,它仍然可以正常工作。即使精度损失是由于一些愚蠢的错误而不是因为正常的舍入误差,它也会这样做。
无论这种行为看起来多么令人惊讶,它都是正确的。或者我相信直到被证明是错误的。