整型转浮点型再转整型的精度损失问题

11

最近,我编写了一个小程序,并使用 mingw32(在 Windows8 上)的两个不同版本进行编译。令人惊讶的是,我得到了两个不同的结果。我尝试反汇编它,但没有发现特别的地方。请问有谁可以帮助我吗?谢谢。

exe 文件: https://www.dropbox.com/s/69sq1ttjgwv1qm3/asm.7z

结果:720720(gcc 版本 4.5.2),720719(gcc 版本 4.7.0)

编译器标志:-lstdc++ -static

代码片段如下:

#include <iostream>
#include <cmath>

using namespace std;

int main()
{
    int a = 55440, b = 13;
    a *= pow(b, 1);
    cout << a << endl;
    return 0;
}

汇编输出(4.5.2):

http://pastebin.com/EJAkVAaH

汇编输出(4.7.0):

http://pastebin.com/kzbbFGs6


1
你为什么写了这么奇怪的代码? - Pubby
1
这是在一个项目中发现的问题,为了测试目的我编写了这段代码。@Pubby - Zhe Chen
结果:720720(gcc版本4.5.2),720719(gcc版本4.7.0)。我真的很想知道问题背后的原因。@JohnSmith - Zhe Chen
2
通常情况下,您应该避免在整数计算中使用浮点数运算。然而,如果确实需要混合使用它们,请在转换为整数时考虑四舍五入:a = int(0.5 + a*pow(b, 1));。我猜测pow(b,1)返回的是12.9999...而不是13。 - comocomocomocomo
2
@chenzhekl,你是如何验证寄存器的内容为“13”的?你能看到扩展精度位吗?也许你的调试器/输出被舍入为标准的“double”类型,显示为“13”,而不是显示扩展精度位。 - edA-qa mort-ora-y
显示剩余5条评论
1个回答

9

我已经能够用单个编译器版本重现这个问题。

我的是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) 指令相同的舍入和精度控制字段,那么可以这样做:

_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() 没有将 131 精确计算为 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() 不同,它需要两个 int 而不是两个 double)。

但我不会因此而责怪 exp()。C++11 除了 C 的 double pow(double, double) 之外还定义了 float pow(float, float)long double pow(long double, long double)。但标准中没有 double pow(int, int)

编译器提供一个整数参数版本并不会对结果的精度做出任何额外保证。如果 exp() 计算 ab

    ab = 2b * log2(a)

或者为

    ab = eb * ln(a)

对于浮点数,过程中肯定会有舍入误差。

如果 "整数" 版本的 exp() 做类似的事情并因舍入误差而导致类似的精度损失,它仍然正常工作。即使精度损失是由于一些愚蠢的错误而不是由于正常的舍入误差引起的。

无论这种行为看起来多么奇怪,都是正确的。除非证明我错了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接