简体   繁体   中英

Why do C and Java round floats differently?

Consider the floating point number 0.644696875. Let's convert it to a string with eight decimals using Java and C:

Java

import java.lang.Math;
public class RoundExample{
     public static void main(String[] args){
        System.out.println(String.format("%10.8f",0.644696875));
     }
}

result: 0.6446968 8

try it yourself: http://tpcg.io/oszC0w

C

#include <stdio.h>

int main()
{
    printf("%10.8f", 0.644696875); //double to string
    return 0;
}

result: 0.6446968 7

try it yourself: http://tpcg.io/fQqSRF

The Question

Why is the last digit different?

Background

The number 0.644696875 cannot be represented exactly as a machine number. It is represented as the fraction 2903456606016923 / 4503599627370496 which has a value of 0.6446968749999999

This is admittedly an edge case. But I am really curious as to the source of the difference.

Related: https://mathematica.stackexchange.com/questions/204359/is-numberform-double-rounding-numbers

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.6446968 699999999774519210404832847416400909423828125 . So eight digits are needed to uniquely distinguish 0.6446968 749999999470645661858725361526012420654296875 .

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM