Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Int to Float to Int conversion precision loss

Recently, I wrote a small program and compiled it using mingw32(on Windows8) of 2 different versions. Surprisingly, I got two different reusults. I tried disassmbling it but found nothing special. Could anyone help me? Thank you.

the exe files: https://www.dropbox.com/s/69sq1ttjgwv1qm3/asm.7z

results: 720720(gcc version 4.5.2), 720719(gcc version 4.7.0)

compiler flags: -lstdc++ -static

Code snipped as following:

#include <iostream>
#include <cmath>

using namespace std;

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

Assembly output(4.5.2):

http://pastebin.com/EJAkVAaH

Assembly output(4.7.0):

http://pastebin.com/kzbbFGs6

like image 452
Zhe Chen Avatar asked Mar 10 '13 06:03

Zhe Chen


1 Answers

I've been able to reproduce the problem with a single version of the compiler.

Mine is MinGW g++ 4.6.2.

When I compile the program as g++ -g -O2 bugflt.cpp -o bugflt.exe, I get 720720.

This is the disassembly of 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

As you can see, the value is calculated at compile time.

When I compile it as g++ -g -O2 -fno-inline bugflt.cpp -o bugflt.exe, I get 720719.

This is the disassembly of 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

If I replace the call to exp() with loading 13.0 like this:

_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

I get 720720.

If I set the same rounding and precision control fields of the x87 FPU control word for the duration of exp() as for the fistpl 4(%esp) instruction like this:

_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

I get 720720 as well.

From this I can only conclude that exp() isn't calculating 131 precisely as 13.0.

It may be worth looking at the source code of that __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) to see exactly how it manages to screw up exponentiation with integers (see, unlike C's exp() it takes two ints instead of two doubles).

But I wouldn't blame exp() for that. C++11 defines float pow(float, float) and long double pow(long double, long double) in addition to C's double pow(double, double). But there's no double pow(int, int) in the standard.

The fact that the compiler provides a version for integer arguments does not make any additional guarantee about the precision of the result. If exp() calculates ab as

    ab = 2b * log2(a)

or as

    ab = eb * ln(a)

for floating-point values, there definitely can be rounding errors in the process.

If the "integer" version of exp() does something similar and incurs a similar loss of precision due to rounding errors, it still does its job right. And it does it even if the loss of precision is due to some silly bug and not because of the normal rounding errors.

However surprising this behavior may seem, it's correct. Or so I believe until proven wrong.

like image 163
Alexey Frunze Avatar answered Oct 22 '22 03:10

Alexey Frunze