Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why double plus sometimes right,sometimes wrong?

Tags:

java

double

I know that java has double precision pitfalls, but why sometimes, the approximation result is ok, but sometimes isn't.

code like this:

for ( float value = 0.0f; value < 1.0f; value += 0.1f )  
    System.out.println( value );

result like this:

0.0
0.1
0.2
0.3
...
0.70000005
0.8000001
0.9000001
like image 952
freeman Avatar asked Nov 27 '12 05:11

freeman


1 Answers

As you state, not all numbers can be represented exactly in IEEE754. In conjunction with the rules that Java uses for printing those numbers, that affects what you'll see.

For background, I'll briefly cover the IEEE754 inaccuracies. In this particular case, 0.1 cannot be represented exactly so you'll often find that the actual number used is something like 0.100000001490116119384765625.

See here for the analysis of why this is so. The reason you're getting the "inaccurate" values is because that error (0.000000001490116119384765625) gradually adds up.


The reason why 0.1 or 0.2 (or similar numbers) don't always show that error has to do with the printing code in Java, rather than the actual value itself.

Even though 0.1 is actually a little higher than what you expect, the code that prints it out doesn't give you all the digits. You'd find, if you set the format string to deliver 50 digits after the decimal, that you'd then see the true value.

The rules for how Java decides to print out a float (without explicit formatting) are detailed here. The relevant bit for the digit count is:

There must be at least one digit to represent the fractional part, and beyond that as many, but only as many, more digits as are needed to uniquely distinguish the argument value from adjacent values of type float.

By way of example, here's some code showing you how this works:

public class testprog {
    public static void main (String s[]) {
        float n; int i, x;
        for (i = 0, n = 0.0f; i < 10; i++, n += 0.1f) {
            System.out.print( String.format("%30.29f %08x ",
                n, Float.floatToRawIntBits(n)));
            System.out.println (n);
        }
    }
}

The output of this is:

0.00000000000000000000000000000 00000000 0.0
0.10000000149011611938476562500 3dcccccd 0.1
0.20000000298023223876953125000 3e4ccccd 0.2
0.30000001192092895507812500000 3e99999a 0.3
0.40000000596046447753906250000 3ecccccd 0.4
0.50000000000000000000000000000 3f000000 0.5
0.60000002384185791015625000000 3f19999a 0.6
0.70000004768371582031250000000 3f333334 0.70000005
0.80000007152557373046875000000 3f4cccce 0.8000001
0.90000009536743164062500000000 3f666668 0.9000001

The first column is the real value of the float, including inaccuracies from IEEE754 limitations.

The second column is the 32-bit integer representation of the floating point value (how it looks in memory rather than its actual integer value), useful for checking the values at the low level bit representation.

The final column is what you see when you just print out the number with no formatting.


Now looking at some more code, which will show you both how the inaccuracies of continuously adding an inexact value will give you the wrong number, and how the differences with surrounding values controls what is printed:

public class testprog {
    public static void outLines (float n) {
        int i, val = Float.floatToRawIntBits(n);
        for (i = -1; i < 2; i++) {
            n = Float.intBitsToFloat(val+i);
            System.out.print( String.format("%30.29f %.08f %08x ",
                n, n, Float.floatToRawIntBits(n)));
            System.out.println (n);
        }
        System.out.println();
    }
    public static void main (String s[]) {
        float n = 0.0f;
        for (int i = 0; i < 6; i++) n += 0.1f;
        outLines (n); n += 0.1f;
        outLines (n); n += 0.1f;
        outLines (n); n += 0.1f;
        outLines (0.7f);
    }
}

This code uses the continued addition of 0.1 to get up to 0.6 then prints out the values for that and adjacent floats. The output of that is:

0.59999996423721310000000000000 0.59999996 3f199999 0.59999996
0.60000002384185790000000000000 0.60000002 3f19999a 0.6
0.60000008344650270000000000000 0.60000008 3f19999b 0.6000001

0.69999998807907100000000000000 0.69999999 3f333333 0.7
0.70000004768371580000000000000 0.70000005 3f333334 0.70000005
0.70000010728836060000000000000 0.70000011 3f333335 0.7000001

0.80000001192092900000000000000 0.80000001 3f4ccccd 0.8
0.80000007152557370000000000000 0.80000007 3f4cccce 0.8000001
0.80000013113021850000000000000 0.80000013 3f4ccccf 0.80000013

0.69999992847442630000000000000 0.69999993 3f333332 0.6999999
0.69999998807907100000000000000 0.69999999 3f333333 0.7
0.70000004768371580000000000000 0.70000005 3f333334 0.70000005

The first thing to look at is that the final column has enough fractional digits in the middle lines of each block to distinguish it from the surrounding lines (as per the Java printing specifications mentioned previously).

For example, if you only had three places after the decimal, you would not be able to distinguish between 0.6 and 0.6000001 (the adjacent bit patterns 0x3f19999a and 0x3f19999b). So, it prints as much as it needs.

The second thing you'll notice is that our 0.7 value in the second block is not 0.7. Rather, it's 0.70000005 despite the fact that there's an even closer bit pattern to that number (on the previous line).

This has been caused by the gradual accumulation of errors caused by adding 0.1. You can see from the final block that, if you just used 0.7 directly rather than continuously adding 0.1, you'd get the right value.

So, in your particular case, it's the latter issue causing you the problems. The fact that you're getting 0.70000005 printed out is not because Java hasn't got a close enough approximation (it has), it's because of the way you got to 0.7 in the first place.

If you modify that code above to contain:

outLines (0.1f);
outLines (0.2f);
outLines (0.3f);
outLines (0.4f);
outLines (0.5f);
outLines (0.6f);
outLines (0.7f);
outLines (0.8f);
outLines (0.9f);

you'll find it can print out all the numbers in that group correctly.

like image 177
paxdiablo Avatar answered Oct 09 '22 13:10

paxdiablo