1

Say I have a float (or double) in my favorite language. Say that in memory this value is stored according to IEEE 754, say that I serialize this value in XML or JSON or plain text using base 10. When serializing and de-serializing this value will I lose precision of my number? When should I care about this precision loss?

Would converting the number to base64 prevent the loss of precision?

Hoffmann
  • 14,369
  • 16
  • 76
  • 91

2 Answers2

6

It depends on the binary-to-decimal conversion function that you use. Assuming this function is not botched (it has no reason to be):

  1. Either it converts to a fixed precision. Old-fashioned languages such as C offer this kind of conversion to decimal. In this case, you should use a format with 17 significant decimal digits. A common format is D.DDDDDDDDDDDDDDDDEXXX where D and X are decimal digits, and there are 16 digits after the dot. This would be specified as %.16e in C-like languages. Converting back such a decimal value to the nearest double produces the same double that was originally printed.
  2. Or convert it to the shortest decimal representation that converts back to the same double. This is what some modern programming languages (e.g. Java) offer by default as printing function. In this case, the property that parsing back the decimal representation will return the original double is automatic.

In either case loss of accuracy should not happen. This is not because you get the exact decimal representation of the original binary64 number with either method 1. or 2. above: in the general case, you don't. Such an exact representation always exists (because 10 is a multiple of 2), but can be up to ~750 digits long for a binary64 number.

What you get with method 1. or 2. above is a decimal number that is closer to the original binary64 number than to any other binary64 number. This means that the opposite conversion, from decimal to binary64, will “round back” to the original.

This is where the “non-botched” assumption is necessary: in order for the successive conversions to return to the original number they must respectively produce the closest decimal to the binary64 number passed and the closest binary64 to the decimal number passed. In these conditions, and with the appropriate number of decimal digits for the first conversion, the round-trip is lossless.


I should point out that (non-botched) conversions to and from decimal are expensive operations. Unless human-readability of the result is important for you, you should consider a simpler format to convert to. The C99-style hexadecimal representation for floating-point numbers is a good compromise between conversion cost and readability. It is not the most compact but it contains only printable characters.

Community
  • 1
  • 1
Pascal Cuoq
  • 79,187
  • 7
  • 161
  • 281
  • 1) is very interesting, I was not aware of that, but aren't there many numbers that can be represented in base 2, but can not be accurately represented in base10? This should be a problem for 2) if that is indeed the case, although I might be confusing myself with numbers like 1/3 that can not be represented as floating points at all (in whatever base it uses). – Hoffmann Jul 03 '14 at 19:52
  • 2
    @Hoffman all numbers with a finite base-2 representation have a finite base-10 representation, but it can have many decimals (~750 decimal digits for double-precision binary floating-point). Regardless of whether you use 1. or 2., you usually don't get this exact representation. You get a decimal number that is closer to the original binary64 number than to any other binary64 number. This means that the opposite conversion, from decimal to binary64, will “round back” to the original. I'll expand my answer in a couple of hours. – Pascal Cuoq Jul 03 '14 at 20:05
  • Hm, interesting, so a Java program printing doubles can sometimes print a >700 digit number? That could be a nuisance in some cases (logs for example.) My guess is that most JSON and XML parsers/encoders would not go to that length for a double, so if using those values in math heavy applications one should mind their encoders as the errors could propagate to meaningful differences. – Hoffmann Jul 03 '14 at 20:53
  • 1
    @Hoffmann No, this is what I tried to explain (now in my answer). Java will not print the exact representation of the double. It will print the shortest decimal representation that is closer to the original double than to any other double. This representation never has more than 17 significant decimal digits. It is sometimes shorter. For instance, the shortest decimal representation for 0.25 is “0.25”, and the shortest decimal representation for the double nearest to 0.1 is “0.1”. – Pascal Cuoq Jul 03 '14 at 20:58
  • @PascalCuoq: Out of curiosity, do you know what promises Java's string-to-double methods make with regard to accuracy? It seems that--unlike the method in .NET--`Double.parseDouble` will read as many digits as are necessary to achieve precise rounding [e.g. 0.845512408225570000208648480111151002347469329833984375 will round up, but 0.845512408225570000208648480111151002347469329833984374999 will round down] but I wonder how that's done? – supercat Jul 08 '14 at 15:47
  • @supercat One way to achieve correct rounding for any number of input (decimal) digits is to switch to “sticky” mode after a certain number of digits. This is how the musl libc does it. http://git.musl-libc.org/cgit/musl/tree/src/internal/floatscan.c#n103 – Pascal Cuoq Jul 08 '14 at 15:56
  • @PascalCuoq: Certainly that would simplify the case of 4503599627370496.5000...one million zeroes...0001, but in the indicated case a change in the 54th significant figure was relevant, and it wasn't part of a run of zeroes or nines. – supercat Jul 08 '14 at 16:10
  • @supercat I didn't say what “a certain number of digits” was. It's not 54. According to this question, ~770 is about right for double-precision: http://stackoverflow.com/questions/17244898/maximum-number-of-decimal-digits-that-can-affect-a-double – Pascal Cuoq Jul 08 '14 at 16:13
  • Pascal, I thought Java printed shortest strings too...until now: http://www.exploringbinary.com/java-doesnt-print-the-shortest-strings-that-round-trip/ – Rick Regan May 01 '16 at 18:31
1

The approach of converting to the shortest form which converts back the same is dangerous (the "round-trip" string formatting mode in .NET uses such an approach, and is buggy as a result). There is probably no reason not to have a decimal-to-binary conversion method yield a result which is more than 0.75lsb from the exact specified numerical value, guaranteeing that a conversion will always yield a perfectly-rounded numerical value is expensive and in most cases not particularly helpful. It would be better to ensure that the precise arithmetic value of the decimal expression will be less than 0.25lsb from the double value to be represented. If a that's less than 0.25lsb away from a double is fed to a routine which returns a double within 0.75lsb of it, the latter routine can be guaranteed to yield the same double as was given to the former.

The approach of simply finding the shortest form that yields the same double assumes that any string representation will always be parsed the same way, even if the value represented falls almost exactly halfway between two adjacent double values. Since obtaining a perfectly-rounded result could require reading an arbitrary number of digits (e.g. 1125899906842624.125000...1 should round up to 1125899906842624.25) few implementations are apt to bother; if an implementation is going to ignore digits beyond a certain point, even when that might yield a result that was e.g. more than .056lsb way from the correct one, it shouldn't be trusted to be accurate to 0.50000lsb in any case.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • 1
    Interesting, so I should bother with this if I am feeding my values to different parsers. If exchanging this value many times the least significant bit could come out wrong occasionally and this could propagate errors across my calculations? – Hoffmann Jul 07 '14 at 16:50
  • 1
    @Hoffmann: Consider the precisely-representable value 0.845512408225570055719799711368978023529052734375; the next smaller `double` value is 0.84551240822556994469749724885332398116588592529296875. Any value less than 0.845512408225570000208648 should round to the latter, but 0.84551240822557 may in some versions of .NET sometimes get rounded to the former (even if 0.845512408225570 would not!). – supercat Jul 07 '14 at 17:14