• Post Reply Bookmark Topic Watch Topic
  • New Topic
programming forums Java Mobile Certification Databases Caching Books Engineering Micro Controllers OS Languages Paradigms IDEs Build Tools Frameworks Application Servers Open Source This Site Careers Other Pie Elite all forums
this forum made possible by our volunteer staff, including ...
Marshals:
  • Campbell Ritchie
  • Jeanne Boyarsky
  • Ron McLeod
  • Paul Clapham
  • Liutauras Vilda
Sheriffs:
  • paul wheaton
  • Rob Spoor
  • Devaka Cooray
Saloon Keepers:
  • Stephan van Hulst
  • Tim Holloway
  • Carey Brown
  • Frits Walraven
  • Tim Moores
Bartenders:
  • Mikalai Zaikin

Precision for double literals

 
Greenhorn
Posts: 2
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Fairly trivial question...



Running this gives the output:

d = 0.8999999999999999
d2 = 0.9

What's bugging me is the second line in the output. It's not possible to store the value 0.9 precisely in the double variable, hence the first line of the output. That's fine. I get that. But, that being the case, I don't really understand why the second line doesn't print "d2 = 0.8999999999999999". How is the value 0.9 being held precisely between lines 2 and 4. I'm just curious about what is happening under-the-hood and I can't seem to find the relevant part of the JLS to explain it.

Thanks in advance.
 
author
Posts: 23951
142
jQuery Eclipse IDE Firefox Browser VI Editor C++ Chrome Java Linux Windows
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

I'm just curious about what is happening under-the-hood and I can't seem to find the relevant part of the JLS to explain it.



The floating point formats (and behavior) is not defined in the JLS. Java uses the IEEE standard for floating points -- 32 bit and 64 bit standard.

Henry
 
Lee Bulmer
Greenhorn
Posts: 2
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

Henry Wong wrote:

I'm just curious about what is happening under-the-hood and I can't seem to find the relevant part of the JLS to explain it.



The floating point formats (and behavior) is not defined in the JLS. Java uses the IEEE standard for floating points -- 32 bit and 64 bit standard.

Henry



OK, I see that in the JLS. But, as I understand it*, it specifies how floating point numbers are encoded into binary. Using this encoding it's not possible to precisely specify the number 0.9 (for example). This causes the rounding error in the assignment to variable d in the example I posted. This same rounding error does not occur in the assignment to variable d2. This leads to to believe that the JVM is not using IEEE 754 to encode the literal 0.9 in the assignment to d2.

If this behaviour is explained somewhere in IEEE 754, I've missed it. I also cannot see anything in the JLS that suggests the JVM ever uses anything other than IEEE 754 to encode floating point numbers. Obviously I've missed something in one of the two specifications. So, if you happen to know a part of IEEE 754 spec that would explains this, I would be very grateful if you could point it out as it's esaping me for the moment.

Thanks for the reply,

Lee.
* I admit my understanding may not be perfect, hence the post.
 
Marshal
Posts: 79178
377
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I have never actually read IEEE754(1985) which I think is now called something different [but I do know what the structure of floating-point numbers is]. But it was unnecessary to explain imprecision; programmers in those days had used slide rules (I still have an old slide rule I bought about 1968) and were familiar with imprecision. Look in our FAQ (no 20) where it gives some useful links.
 
Bartender
Posts: 4179
22
IntelliJ IDE Python Java
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
It also might be worth noting that with the simple example given above, the compiler can do some optimizations because it knows the value of d is calculated from two constants when assigned. Which means the results is a constant. Which means the math can be done at compile time rather than run time. On the other hand, the value of d and d2 are never changed, so they can be replaced by constants as well. Given this code:

You can get this byte code (un-important stuff cleaned out)


So when d is encountered it is treated like the constant 0.89999...d, while when d2 is encountered it is treated as the constant 0.9d. Not sure if it helps, or is exactly pertinent to your question, but it is something to keep in mind when making these sorts of comparisons.
 
Greenhorn
Posts: 3
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I know this is super old, but I found this post when asking the same question.

The answer is that the first 15 decimal places are guaranteed to be correct and the remaining decimal places are likely to have precision errors in the binary representation of the double.  So Java automatically truncates double values to the first 15 decimal places when displaying them.

0.9 will display as 0.9 and 0.900000000000009 will display as 0.900000000000009 but 0.9000000000000009 will display as 0.9 because that 16th decimal place of 9 gets truncated.

In actuality, the double value of 0.9 is stored in binary as 0011111111101100110011001100110011001100110011001100110011001101 which converts to decimal as is 0.90000000000000002220446049250313080847263336181640625

Java is just hiding the messiness at the end of the number from you because it knows that those values are probably just noise.

But when you do math with that number, the messiness can't be ignored and it often messes up the values in the first 15 decimal places.
 
Marshal
Posts: 28193
95
Eclipse IDE Firefox Browser MySQL Database
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

Justin May wrote:But when you do math with that number, the messiness can't be ignored and it often messes up the values in the first 15 decimal places.



Welcome to the Ranch!

Yes, that's one way of looking at it. But it indicates that you are thinking of calculations in base-10, or decimal, numbers. But the IEEE specifications were written for people like engineers, who might be doing things like launching satellites to Pluto and who therefore need accurate calculations (rather than rounding to decimal values, which introduces errors). If you want to do calculations with decimal numbers (instead of binary) and few decimal places -- like money -- then BigDecimal is what you should be using.

(Back in the 20th century when Java was invented, the engineers had been using computers for decades already but the money guys were new on the scene. That's probably why the Java designers didn't include decimal primitives in the language.)
 
Campbell Ritchie
Marshal
Posts: 79178
377
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Welcome to the Ranch

Justin May wrote:. . . the first 15 decimal places are guaranteed to be correct

I am afraid that isn't correct. You cannot be sure even that there are fifteen correct decimal sig. fig. in a double represented in decimal. The problem becomes particularly bad with small magnitude (absolute value) numbers, smaller than this. When you get down to the smallest subnormal numbers, you cannot necessarily rely on finding the first decimal digit correct.

Campbell's JShell wrote:jshell> Double.MIN_VALUE
$5 ==> 4.9E-324

jshell> Double.MIN_VALUE * 1.4
$6 ==> 4.9E-324

jshell> Double.MIN_VALUE * 1.5
$7 ==> 9.9E-324

. . .  So Java automatically truncates double values to the first 15 decimal places when displaying them. . . .

It is not the language that decides how to display a double, but methods, including this one. It tells you that it prints as many digits as are necessary to distinguish a number from other numbers (i.e. adjacent values).
 
Justin May
Greenhorn
Posts: 3
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

The problem becomes particularly bad with small magnitude (absolute value) numbers, smaller than this.



That's a good point, but for double literal values inside the range of 10^-3 and 10^7 it seems that Java ('s Double.toString method) will repeat back to you whatever you give it as long as it has <16 decimal places (and is >= 0.001). Outside of that range is converts them so "scientific-y notation", so that's its own bag of wax.

The documentation says (for literal values between 10^-3 and 10^7, like 0.9 is):

How many digits must be printed for the fractional part of m or a? 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 double. That is, suppose that x is the exact mathematical value represented by the decimal representation produced by this method for a finite nonzero argument d. Then d must be the double value nearest to x; or if two double values are equally close to x, then d must be one of them and the least significant bit of the significand of d must be 0.



That sounds more complicated than "the first 15 decimal places" for sure, but what does it actually mean? Why does that make System.out.print(0.9) output 0.9 instead of 0.90000000000000002220446049250313080847263336181640625 ?

Why does that make System.out.print(0.900000000000009) output 0.900000000000009 but System.out.print(0.9000000000000009) outputs 0.9 ?

What does it mean for a value to "uniquely distinguish [itself] from an adjacent value of type double" ? Why is 0.9000000000000009 adjacent to 0.9 but 0.900000000000009 is not adjacent to 0.9?

In binary they are:
0 01111111110 1100110011001100110011001100110011001100110011001101 // 0.9
0 01111111110 1100110011001100110011001100110011001100110011010101 // 0.9000000000000009
0 01111111110 1100110011001100110011001100110011001100110100011110 // 0.900000000000009

What makes the first two doubles "adjacent" so they output the same value, but the 1st and 3rd one not adjacent? I can see that the first two have very close mantissa's, but they aren't +/- 1, they are different by 1000(base2) and the first and 3rd are different by 10001(base2).
 
Master Rancher
Posts: 4806
72
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

Justin May wrote:Why does that make System.out.print(0.900000000000009) output 0.900000000000009 but System.out.print(0.9000000000000009) outputs 0.9 ?


Ummm, System.out.print(0.9000000000000009) actually prints 0.9000000000000009.  Try it.  You repeated the same number both times.

If you meant to add one more 0 in the middle, then 0.90000000000000009 prints as 0.9000000000000001 - rounding, not truncating, to 16 digits .  It's still distinct from the other numbers you mention.

I'm going to add '_' separators between the digits to try to keep from going insane here.

Justin May wrote:What does it mean for a value to "uniquely distinguish [itself] from an adjacent value of type double" ? Why is 0.900_000_000_000_000_9 adjacent to 0.9 but 0.900_000_000_000_009 is not adjacent to 0.9?


Well, looking at the bit patterns you provided shows the answer.    None of those bit patterns are adjacent to each other.  Other values exist which are in between the ones you've chosen to talk about.  These values all have the same sign and exponent, so you just need to compare the mantissas, which are the last 53 bits in each one.  Those values all have differences 6-8 bits from the end.

Justin May wrote:In binary they are:
0 01111111110 1100110011001100110011001100110011001100110011001101 // 0.9
0 01111111110 1100110011001100110011001100110011001100110011010101 // 0.900_000_000_000_000_9
0 01111111110 1100110011001100110011001100110011001100110100011110 // 0.900_000_000_000_009



The actual values adjacent to 0.9 are:

I didn't bother finding the neighbors of the other two.  If you want to, Math.nextDown() and Math.nextUp() are helpful methods.

In this case, the adjacent values happen have a difference of 1, in the 16th decimal digit.  (Well, ignoring the fact that a bunch of higher-order digits change from 9 to 0, but it's triggered by a difference in the 16th digit.)   That isn't always the case, but it often is.  Sometimes they may skip some 16-digit decimal values - the accuracy of the 16th digit is a little shaky.

Now I can go back and answer an earlier question:

Justin May wrote:That sounds more complicated than "the first 15 decimal places" for sure, but what does it actually mean? Why does that make System.out.print(0.9) output 0.9 instead of 0.90000000000000002220446049250313080847263336181640625 ?


Even though the binary representation of 0.9 actually more precisely represents 0.90000000000000002220446049250313080847263336181640625, we just don't need all those digits.  Since the nearest other two values are

0.899_999_999_999_999_9

and

0.900_000_000_000_000_1

we only need 16 digits of

0.900_000_000_000_000_022_204_460_492_503_130_808_472_633_361_816_406_25

to get

0.900_000_000_000_000_0

which is more easily written

0.9
 
Justin May
Greenhorn
Posts: 3
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Awesome, thanks!

I was totally testing with 1.550_000_000_000_001 and 1.550_000_000_000_000_1 originally, which behaves as I claimed the 0.900...009 values did. I could have sworn I tested those values in JShell before posting, but as you could tell I typed all those zero's by hand rather than copy and pasting, so who knows what I typed into jshell.

I'm glad we got to the bottom of this! Hopefully someone else will stumble on this post in another 13 years and it will answer their question!
reply
    Bookmark Topic Watch Topic
  • New Topic