1

Edit: I know floating point arithmetic is not exact. And the arithmetic isn't even my problem. The addition gives the result I expected. 8099.99975f doesn't.


So I have this little program:

public class Test {
    public static void main(String[] args) {
        System.out.println(8099.99975f); // 8099.9995
        System.out.println(8099.9995f + 0.00025f); // 8100.0
        System.out.println(8100f == 8099.99975f); // false
        System.out.println(8099.9995f + 0.00025f == 8099.99975f); // false
        // I know comparing floats with == can be troublesome
        // but here they really should be equal in every bit.
    }
}

I wrote it to check if 8099.99975 is rounded to 8100 when written as an IEEE 754 single precision float. To my surprise Java converts it to 8099.9995 when written as a float literal (8099.99975f). I checked my calculations and the IEEE standard again but couldn't find any mistakes. 8100 is just as far away from 8099.99975 as 8099.9995 but the last bit of 8100 is 0 which should make it the right representation.

So I checked the Java language spec to see if I missed something. After a quick search I found two things:

  • The Java programming language requires that floating-point arithmetic behave as if every floating-point operator rounded its floating-point result to the result precision. Inexact results must be rounded to the representable value nearest to the infinitely precise result; if the two nearest representable values are equally near, the one with its least significant bit zero is chosen.

  • The Java programming language uses round toward zero when converting a floating value to an integer [...].

I noticed here that nothing was said about float literals. So I thought that float literals maybe are just doubles which when cast to float are rounded to zero similarly to the float to int casting. That would explain why 8099.99975f was rounded to zero.

I wrote the little program you can see above to check my theory and indeed found that when adding two float literals that should result in 8100 the correct float is computed. (Note here that 8099.9995 and 0.00025 can be represented exactly as single floats so there's no rounding that could lead to a different result) This confused me since it didn't make much sense to me that float literals and computed floats behaved differently so I dug around in the language spec some more and found this:

A floating-point literal is of type float if it is suffixed with an ASCII letter F or f [...]. The elements of the types float [...] are those values that can be represented using the IEEE 754 32-bit single-precision [...] binary floating-point formats.

This ultimately states that the literal should be rounded according to the IEEE standard which in this case is to 8100. So why is it 8099.9995?

phuclv
  • 37,963
  • 15
  • 156
  • 475
mmaag
  • 1,534
  • 2
  • 18
  • 24
  • 1
    `println` is a dirty liar - I suspect it is rounding unexpectedly and throwing off the test-case results/observations. Try with an explicit String.format. – user2864740 Nov 25 '13 at 01:17
  • That is, `println(String.format("%.20f", v))` should result in representative display values. – user2864740 Nov 25 '13 at 01:23
  • If `println` was the problem, I'd still expect `8099.9995f + 0.00025f == 8099.99975f` to be true. And I know that floating point arithmetic isn't exact. I know that my result is not going to be exactly 8099.99975. But the first two numbers are represented exactly. They aren't rounded or anything. I don't see why the addition of two numbers doesn't result in the same result as the decimal to single float conversion. – mmaag Nov 25 '13 at 01:30
  • The key to understanding this sort of thing is to look at the exact values of the floats. Try printing `new BigDecimal(8099.99975f).toString()` etc. The BigDecimal constructor and its toString() are both exact. – Patricia Shanahan Nov 25 '13 at 03:09
  • Here we have yet another question incorrectly closed as a duplicate. This question specifically involves not just the behavior of the floating-point arithmetic system but how Java displays floating-point values. This latter is not addressed in the purported original question or its answers. – Eric Postpischil Nov 25 '13 at 10:47
  • `8099.99975f` is converted to the nearest floating-point value, which is a number very slightly greater than 8099.9995, which is why it is selected instead of 8100. However, when Java `println` converts floating-point values to decimal, it does not generally show all digits. It shows just enough digits that converting back to floating-point shows the same value. Thus you see only “8099.995”' but the actual value is slightly greater. – Eric Postpischil Nov 25 '13 at 10:52
  • @Eric I agree entirely with both of your comments. That other question is absolutely NOT a duplicate of this one. It seems that the people who closed the question didn't bother to read both questions. I have voted to re-open and I urge others to do the same. Moreover, if this question DOES get re-opened, please convert your second comment to an answer, and I will upvote it. Whereas it's similar to my answer, it does give a slightly different perspective, which I consider valuable. – Dawood ibn Kareem Nov 25 '13 at 10:56
  • @DavidWallace: I would suggest just adding to your good answer. You could give the specific Java rule and explain how to see the full value (or otherwise change the display from Java’s default). That gives readers knowledge they can use to understand, probe, and control the behaviors involved. I can provide the Java rule if you like; I have quoted it from the specification in old answers but am on a small device at the moment and do not want to dig it up this way. – Eric Postpischil Nov 25 '13 at 11:02
  • @EricPostpischil I'll give it some time. If sanity prevails and this question gets re-opened, I think an answer from you would be a welcome addition to the two that are already here. If it hasn't been re-opened in a day or two, I'll edit your remarks into my answer, giving you due credit of course. – Dawood ibn Kareem Nov 25 '13 at 11:06
  • @DavidWallace: Here is the Java [toString documentation](http://docs.oracle.com/javase/7/docs/api/java/lang/Float.html#toString(float)) for the `float` type, which is what is used for `println`. The wording 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`.” – Eric Postpischil Nov 25 '13 at 13:16

2 Answers2

5

The key point to realise is that the value of a floating point number can be worked out in two different ways, that aren't in general equal.

  • There's the value that the bits in the floating point number give the exact binary representation of.
  • There's the "decimal display value" of a floating point number, which is the number with the least decimal places that is closer to that floating point number than any other number.

To understand the difference, consider the number whose exponent is 10001011 and whose significand is 1.11111010001111111111111. This is the exact binary representation of 8099.99951171875. But the decimal value 8099.9995 has fewer decimal places, and is closer to this floating point number than to any other floating point number. Therefore, 8099.9995 is the value that will be displayed when you print out that number.

Note that this particular floating point number is the next lowest one after 8100.

Now consider 8099.99975. It's slightly closer to 8099.99951171875 than it is to 8100. Therefore, to represent it in single precision floating point, Java will pick the floating point number which is the exact binary representation of 8099.99951171875. If you try to print it, you'll see 8099.9995.

Lastly, when you do 8099.9995 + 0.00025 in single precision floating point, the numbers involved are the exact binary representations of 8099.99951171875 and 0.0002499999827705323696136474609375. But because the latter is slightly more than 1/2^12, the result of addition will be closer to 8100 than to 8099.99951171875, and so it will be rounded up, not down at the end, making it 8100.

Dawood ibn Kareem
  • 77,785
  • 15
  • 98
  • 110
  • Ugh, the whole problem was that I used a binary to decimal converted to look for the next closest floats to 8100. It told me that was 8099.9995. I expected that to be the exact value not some inexact but shorter one. So my whole reasoning was based on the assumption that 8099.9995 can actually be represented exactly. Thanks for pointing out that this is not the case. – mmaag Nov 25 '13 at 10:51
  • 2
    Notice that 0.9995 = 1999/2000. But since 2000 is not a power of two, that fraction can't possibly have a terminating binary representation. In other words, there can be no floating point number, to ANY level of precision, which is exactly 0.9995. Adding 8099 makes no difference to that. No matter what precision you use, a floating point number that approximates 8099.9995 can never be EXACTLY that number. – Dawood ibn Kareem Nov 25 '13 at 11:00
3

The decimal value 8099.99975 has nine significant digits. This is more than can be represented exactly in a float. If you use the floating point analysis tool at CUNY you'll see that the binary representation closest to 8099.9995 is 45FD1FFF. When you attempt to add 0.00025 you are suffering a "loss of significance". In order not to lose significant (left-hand) digits of the larger number, the significand of the smaller has to be shifted right to match the scale (exponent) of the larger. When this happens, its value becomes ZERO as it shifts off the right end of the register.

Decimal     Exponent        Significand
---------   --------------  -------------------------
8099.9995   10001011 (+12)  1.11111010001111111111111
   0.00025  01110011 (-12)  1.00000110001001001101111

To line these up for addition, the second one has to shift right 24 bits, but there are only 23 bits in the significand of a single-precision float. The significand disappears, leaving zero, so the addition has no effect.

If you want this to work, switch to double-precision arithmetic.

Jim Garrison
  • 85,615
  • 20
  • 155
  • 190
  • As I said my actual question is "why isn't `8099.99975f` converted to `8100`?" But you actually pointed out something different which I hadn't even thought about: `8099.9995f + 0.00025f` should be `8099.9995f`, right? As you said, the lower value gets shifted until it's zero. Then why is Java telling me it's `8100`? Is it using doubles even if I explicetly tell it to use floats? – mmaag Nov 25 '13 at 02:09
  • This answer is wrong; adding `.00025f` to `8099.9995f` produces 8100, not the value for `8099.9995f`. – Eric Postpischil Nov 25 '13 at 11:07
  • The source text `8099.9995f` produces a value very slightly greater than 8099.9995. Then adding `.00025f` produces (mathematically, before rounding in floating point) a value closer to 8100 than to the value near 8099.9995. Then rounding produces 8100. Bits “below” the significand are not ignored; they affect rounding. – Eric Postpischil Nov 25 '13 at 11:10