5

I'm writing code in different languages that involves double precision arithmetic. Ideally the programs need to yield exactly the same values. I'm aware that not all double/float arithmetic is deterministic (explained nicely here: https://randomascii.wordpress.com/2013/07/16/floating-point-determinism/), so I need to be carefeful. Can someone explain what is going on here though?:

C program:

#include <stdio.h>
#include <stdlib.h>

int
main(void) {
    printf("%.52f\n", 1.66007664274403694e-03);
    return (EXIT_SUCCESS);
}

Result: 0.0016600766427440369430584832244335302675608545541763

"Equivalent" Java 8 program:

class A {
    public static void main(String args[]) {
        System.out.printf("%.52f\n", 1.66007664274403694e-03D);
    }
}

Result: 0.0016600766427440370000000000000000000000000000000000

The results are different. I have a feeling that this may be related to floating point rounding modes, however, as far as I can see C and Java have the same defaults (?).

How can I ensure the two programs have the same result?

EDIT:

FWIW, If I print the constant as a BigDecimal: System.out.printf("%.52f\n", new BigDecimal(1.66007664274403694e-03));, I get: 0.0016600766427440369430584832244335302675608545541763. This might prove that this is not a display issue, but who knows what magic the JVM does underneath.

EDIT2:

Using strictfp as @chris-k suggests, I annotated the class, and the result remains as 0.0016600766427440370000000000000000000000000000000000.

EDIT3:

Another suggestion was to try System.out.printf("%.52f\n", new BigDecimal("1.66007664274403694e-03")); which gives a result we have not seen yet: 0.0016600766427440369400000000000000000000000000000000.

Edd Barrett
  • 3,425
  • 2
  • 29
  • 48
  • The number is rounded at 18th place after decimal , not a big issue as far as concerned . – ameyCU Oct 20 '15 at 10:08
  • 1
    you may use `BigDecimal` for more accurate calculations in java – Andrew Tobilko Oct 20 '15 at 10:09
  • Yes, I could use BigDecimal at the cost of efficiency, but since doubles are standardised, how comes there is a discrepancy? – Edd Barrett Oct 20 '15 at 10:11
  • Because floating point formats between platforms and even between different programming languages on the same platform don't necessarily need to be the same. – Some programmer dude Oct 20 '15 at 10:12
  • 1
    Also, C uses [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point) for storing floating point values, and for [double precision](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) that only gives you 15-17 significant decimal digits, far from the 52 you print out. – Some programmer dude Oct 20 '15 at 10:22
  • It seems to me that the C output is actually less correct than the Java output. The latter simply rounds up the last two digits, while the former invents additional digits that the original floating point constant does not contain. Your constant does not require 52 decimal digits of precision, so padding with 0's in the output seems sensible, although it would be desirable not to lose the last two digits to rounding. – Thomas Oct 20 '15 at 10:39
  • 1
    It seems to me like the discrepancy is in `System.out.printf` vs `printf`, not in the storage of `double` values in java vs c. – Klas Lindbäck Oct 20 '15 at 10:44
  • @Thomas I think you don't understand FP representation. It's a _binary_ representation; if you try to print an exact decimal representation, it will likely be infinitely long. There aren't _exactly_ 52 decimal digits of precision. – davmac Oct 20 '15 at 10:46
  • I agree that this is looking like a display problem. But to rule out a representation problem, print out the bits of the double value in both C and Java. Do we actually have the same value after the compiler has done its magic? If yes, then we have a printf issue. If no, then we are comparing how the double was created in the first place. – Chris K Oct 20 '15 at 10:50
  • Checking for equality by *decimal representation* is so full of pitfalls. Too many uncertainities, ranging from what did the *compiler* make of my literal to how the actual *string conversion* works on each platform. Eliminate these by directly dumping the raw binary representation of the double of both platforms (e.g. use Double.doubleToRawLongBits on the java side and print its *hex* value). – Durandal Oct 20 '15 at 11:07
  • @davmac I think you misunderstood the point I was trying to make: if you look at the code samples and at the printed output, the Java one seems to be closer to the *intended* output. The fact that rational numbers are internally represented as floating point numbers is of course correct, but that does not change my point. – Thomas Oct 20 '15 at 14:07
  • @Thomas, Maybe I did understand you, yes. I was thrown off when you stated that the C output "invents additional digits that the original floating point constant does not contain" - because of course the digits aren't really _invented_. Java's "more correct" behavior is just due to its `printf` rounding to the nearest Nth decimal digit of precision for some N where N is less-or-equal to the decimal precision that the representation actually provides. I would argue this is less correct since OP asked for 52 digits of precision, but I guess that's a bit subjective. – davmac Oct 20 '15 at 15:17
  • @Thomas (I meant "maybe I did _mis_understand you" in the comment above). – davmac Oct 20 '15 at 15:46
  • @davmac Yeah, but I do see your point about the additional digits being "more correct" than premature rounding, too. – Thomas Oct 20 '15 at 17:52
  • I've ran into the same issue and noticed that https://babbage.cs.qc.cuny.edu/IEEE-754.old/Decimal.html does the same type of rounding as Java. Haven't concluded why though. – Alexander Torstling Jan 08 '21 at 13:40

3 Answers3

2

I think this is a display issue. The precision specifier in System.out.printf is not allowing the precision to be increased beyond a certain point (after that, it just prints trailing 0 digits).

If you don't care so much about the printed value and just want it to be the same between the two, extract the sign/mantissa/exponent and print them, eg in Java:

    long mantissa = Double.doubleToLongBits(f) & 0x000fffffffffffffL;
    long exponent = Double.doubleToLongBits(f) & 0x7ff0000000000000L;
    long sign = Double.doubleToLongBits(f)  & 0x8000000000000000L;

    // Exponent is encoded with a bias, so to see the actual exponent
    // value:
    exponent >>= (13 * 4);
    exponent -= 1023;

    // Leading 1 bit for mantissa isn't stored:
    mantissa |= 0x0010000000000000L;

    if (sign != 0) sign = 1;
    System.out.println("" + sign + "/" + mantissa + "/" + exponent);

For Java this prints:

    0/7655752242860553/-10

Equivalent C code (works with GCC on x86-64):

#include <string.h>
#include <stdio.h>

int main(int argc, char ** argv)
{
    double f = 1.66007664274403694e-03;

    long long doubleBits;
    memcpy(&doubleBits, &f, sizeof(long long));

    long long mantissa = doubleBits & 0x000fffffffffffffLL;
    long long exponent = doubleBits & 0x7ff0000000000000LL;
    long long sign = doubleBits  & 0x8000000000000000LL;

    // Exponent is encoded with a bias, so to see the actual exponent
    // value:
    exponent >>= (13 * 4);
    exponent -= 1023;

    // Leading 1 bit for mantissa isn't stored:
    mantissa |= 0x0010000000000000L;

    if (sign != 0) sign = 1;
    printf("%lld/%lld/%lld\n", sign, mantissa, exponent);

    return 0;
}

On my machine, it produces the same output. This shows that the internal representation of the constant is the same on both platforms (of course this might not be universally true across architectures and implementations). The discrepancy in the output from your original programs is due to differences in the implementation of the printf functions. (The Java version apparently stops calculating digits after some point and prints 0's instead, possibly to avoid printing floating point constants differently to how they are written in the source code).

On the other hand if this were a calculated value rather than a constant you are likely to have a more difficult time getting the value between C and Java to match :)

davmac
  • 20,150
  • 1
  • 40
  • 68
  • *maybe* my edit proves this is not a display issue(?) – Edd Barrett Oct 20 '15 at 10:35
  • @EddBarrett no, it doesn't. You have still gone through a double before passing to the BigDecimal. Change the arg past into the constructor to a string to prevent the double from changing the value. – Chris K Oct 20 '15 at 10:38
  • Then the result is then `0.0016600766427440369400000000000000000000000000000000`, i.e. a new variation. – Edd Barrett Oct 20 '15 at 10:41
  • @EddBarrett Rar. Don't you love working with floating point numbers :) – Chris K Oct 20 '15 at 10:43
  • Well, this question has certainly opened my eyes :) – Edd Barrett Oct 20 '15 at 10:44
  • @EddBarrett converting a double to a BigDecimal still has a representation problem. A binary floating-point value will often have no exact decimal representation. The conversion has to draw the line somewhere... – davmac Oct 20 '15 at 10:51
  • @davmac Every number that has a finite width binary expansion, including all finite binary floating point numbers, has an exact BigDecimal representation. `x/(2**y)` is equal to `(x * (5**y))/(10**y)` where `**` is exponent. It does not work the other way round because 5 is not a factor of any power of 2. – Patricia Shanahan Oct 20 '15 at 22:37
  • @PatriciaShanahan right, I've mixed up the direction. But in any case the fact that converting to BigDecimal and then outputting as a string can give the same output as C's `printf` just acts as evidence that the internal representation of a double between Java and C _is_ in this case the same, rather than is _not_ the same, since the BigDecimal value originates from the double value - and that therefore the issue described by Edd is due to the conversion to decimal form. – davmac Oct 20 '15 at 22:48
2

You could very well have a display issue here only. However during calculations Java is free to differ from C's behaviour.

Java is free to use extra precision in a floating point calculation if it is available on a platform. However Java does offer an override, declare your double as being strictfp and that will inform the JVM to restrict the calculations making them more portable.

You may also want to check out the class StrictMath in Java, it also has a few utils that have been implemented in C for compatibility purposes.

Community
  • 1
  • 1
Chris K
  • 11,622
  • 1
  • 36
  • 49
  • I've edited the question, adding the result of your suggestion. – Edd Barrett Oct 20 '15 at 10:34
  • Jave is not allowed, in general, to use extra precision, only a wider exponent range. See JLS: [15.4. FP-strict Expressions](https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.4). C, on the other hand, does allow extra precision. – Patricia Shanahan Oct 20 '15 at 19:28
0

Use BigDecimal in Java for such operations:

    BigDecimal x = new BigDecimal(1.66007664274403694e-03D);
    System.out.printf(x.toPlainString());

A primitive float in Java is only 4 bytes wide.

mwe
  • 3,043
  • 2
  • 11
  • 16