Why do C and Java round floats differently?

Likely what's happening here is they're using slightly different methods for converting the number to a string, which introduces a rounding error. It's also possible that the method by which the string is converted to a float during compilation is different between them, which again, can give slightly different values due to rounding.

Remember though, float has 24 bits of accuracy for its fraction, which comes out to ~7.22 decimal digits [log10(2)*24], and the first 7 digits agree between them, so it's just the last few least significant bits that are different.

Welcome to the fun world of Floating Point Math, where 2+2 does not always equal 4.


Conclusion

The Java specification requires a troublesome double rounding in this situation. The number 0.6446968749999999470645661858725361526012420654296875 is first converted to 0.644696875 and then rounded to 0.64469688.

In contrast, the C implementation simply rounds 0.6446968749999999470645661858725361526012420654296875 directly to eight digits, producing 0.64469687.

Preliminaries

For Double, Java uses IEEE-754 basic 64-bit binary floating-point. In this format, the value nearest the number in the source text, 0.644696875, is 0.6446968749999999470645661858725361526012420654296875, and I believe this is the actual value to be formatted with String.format("%10.8f",0.644696875).1

What the Java Specification Says

The documentation for formatting with the Double type and f format says:

… If the precision is less than the number of digits which would appear after the decimal point in the string returned by Float.toString(float) or Double.toString(double) respectively, then the value will be rounded using the round half up algorithm. Otherwise, zeros may be appended to reach the precision…

Let’s consider “the string returned by … Double.toString(double)”. For the number 0.6446968749999999470645661858725361526012420654296875, this string is “0.644696875”. This is because the Java specification says that toString produces just enough decimal digits to uniquely distinguish the number within the set of Double values, and “0.644696875” has just enough digits in this case.2

That number has nine digits after the decimal point, and "%10.8f" requests eight, so the passage quoted above says “the value” is rounded. Which value does it mean—the actual operand of format, which is 0.6446968749999999470645661858725361526012420654296875, or that string it mentions, “0.644696875”? Since the latter is not a numeric value, I would have expected “the value” to mean the former. However, the second sentence says “Otherwise [that is, if more digits are requested], zeros may be appended…” If we were using the actual operand of format, we would show its digits, not use zeros. But, if we take the string as a numeric value, its decimal representation would have only zeros after the digits shown in it. So it seems this is the interpretation intended, and Java implementations appear to conform to that.

So, to format this number with "%10.8f", we first convert it to 0.644696875 and then round it using the round half up rule, which produces 0.64469688.

This is a bad specification because:

  • It requires two roundings, which can increase the error.
  • The roundings occur in hard-to-predict and hard-to-control places. Some values will be rounded after two decimal places. Some will be rounded after 13. A program cannot easily predict this or adjust for it.

(Also, it is a shame they wrote zeros “may be” appended. Why not “Otherwise, zeros are appended to reach the precision”? With “may”, it seems like they are giving the implementation a choice, although I suspect they meant the “may” is predicated on whether zeros are needed to reach the precision, not on whether the implementor chooses to append them.)

Footnote

1 When 0.644696875 in the source text is converted to Double, I believe the result should be the nearest value representable in the Double format. (I have not located this in the Java documentation, but it fits the Java philosophy of requiring implementations to behave identically, and I suspect the conversion is done in accordance with Double.valueOf(String s), which does require this.) The nearest Double to 0.644696875 is 0.6446968749999999470645661858725361526012420654296875.

2 With fewer digits, the seven-digit 0.64469687 is insufficient because the Double value closest to it is 0.6446968699999999774519210404832847416400909423828125. So eight digits are needed to uniquely distinguish 0.6446968749999999470645661858725361526012420654296875.