It's really surprising, how long it took to find the most efficient algorithms for double-conversions. In 1980 Coonen already published a good algorithm, but that one was kind-of lost.
For a long time Steele & White's algorithm was the state-of-the-art (with some improvements here and there over time).
Now, Ryu is by far the fastest algorithm out there, but that took ages...
Unfortunately, there doesn't seem to be an easy-to-use, complete library for Ryu yet.
The Grisu library (https://github.com/google/double-conversion) is probably still the go-to library if you don't want to implement it again...
https://dl.acm.org/citation.cfm?id=3192369
"Ryū: fast float-to-string conversion", Ulf Adams 2018
"Section 5 reviews the existing literature. All of the early ap- proaches require arbitrary precision arithmetic in the general case. More recent developments work with xed precision arithmetic, which is faster, but can increase the complexity of the code. By contrast, Ryu ̄ is simple and fast."
Also interesting:
"We did not compare our implementation against the C standard library function printf, as its specification does not include the correctness criteria set forth by Steele and White [15], and, accordingly, neither the glibc nor the MacOS implementation does."
"Correct" conversion, where the length doesn't matter (which could be called "fixed length" for a certain length that is big enough) is easier, because one can avoid a lot of rounding and imprecision issue: using the right technique, just produce enough digits until the imprecisions don't matter anymore.
However, fixed length is as difficult as the shortest, if the length is limited, since the last digit sometimes lies on the boundary and thus runs into the same difficulties as the shortest digit. Think of it this way: needing to decide whether 6 digits is enough, is often similar to asking whether the 6th digit is a 0 or a 9 (roughly speaking). This means that producing the best fixed-length representation (for length 6) runs into the exactly same question.
Internally it uses the same algorithm, Grisu, (with some additional optimizations) so there shouldn't be any difference in output.
If it's easier to use for you, just go with it!
I'm guessing the double-conversion library is a bit more flexible, but I could be wrong.
An interesting aspect of decimal formatting is how it frequently masks representation error (i.e encoding of 0.1 etc), because the error is symmetrical in the formatter/parser it makes such non-representable fractions appear to be stored perfectly to unsuspecting users.
This can be quite deceptive, if more users were aware of just how many of the simple rational decimals they input were converted into imprecise representations they probably wouldn't trust computers as much as they do. To confuse things more, when operating upon periodic representations the result often matches the representation error of the the equivalent accurate decimal value encoded directly (i.e there were errors, but everything canceled out through formatting) - when they occasionally do not (e.g 0.1 + 0.2) it makes the problem appear all the more elusive.
I think this detail is often lost in explanations of 0.1 + 0.2, that is: representation error is extremely common, 0.1 + 0.2 is merely one of the cases where it both persists through the formatter AND you notice it because the inputs were short decimals, and it's so obvious that the output should be a non-periodic decimal..
TL;DR formatting floats to decimals makes us trust floating point math far more than we should - it's healthy to remember that the formatting process is necessarily imprecise and that you are merely looking at a proxy for the underlying value. Remember that next time you look at _seemingly_ non-periodic decimal output.
When I format a float out to 5 decimal places, I'm sort of making a statement that anything beyond that doesn't matter to me.
If you think this isn't precise enough for you, maybe you don't really understand your precision needs.
I disagree. People overestimate the accuracy of decimal encoding so much more than they ever overestimate floating point. Then when they see 0.1 + 0.2 they tend to learn entirely the wrong lesson, and start underestimating the accuracy of floating point alone.
> TL;DR formatting floats to decimals makes us trust floating point math far more than we should
The only reason a decimal encoding can cause too much trust is because we trust decimal too much. Decimal fails in exactly the same ways.
Another way to phrase it is that a single float value represents a range of real numbers, rather than a single real number. So 0.1 stored as a double precision float really represents the range of real numbers from roughly 0.09999999999999999862 to 0.10000000000000001249.
e.g., when I've done my own binary formats, I typically also create a "dump" utility that converts the file to a readable text dump. I find the ergonomics of this are just fine for my purposes.
less somefile.json
isn't that much easier to type than dumpdat somefile.dat | less
That said, different purposes are different. I'm not so worried about human-readability for short-lived stuff, but, if you're talking about data that may be sitting around for decades, then the human readability question becomes something you might better characterize as "future comprehensibility": If someone's trying to dust off 50-year-old business data from deep in the archives, they're going to have a much higher chance of success if it's CSV files than if it's in some custom binary format whose documentation was lost 40 years ago.In fact, thinking about it now, I bet Wireshark would be awesome for that.
> printf ("%a\n", M_PI);
0x1.921fb54442d18p+1
> printf ("%a\n", 42.0);
0x1.5p+5Sometimes you do need to reliably communicate a floating-point number across a text channel. Hex float is low-risk in the sense that both the encoding and decoding process are much simpler and therefore easier to verify than something that round-trips through decimal. Yes, there are some correct implementations of the decimal<->binary algorithms that round-trip correctly. But there also remain a number of incorrect implementations.
Edit: someone else mentioned that roundtrip accuracy could also be an issue. Theoretically true, I guess, but really, if you want to use decimal-based float input you should just use an algorithm that roundtrips correctly, and eat that overhead. Anything else has a potential to impact reproducibility of results, etc. It's not worth it silently corrupting your data just for that saving in compute cost.
It's a trade-off of CPU usage versus flexibility. It's not surprising that textual serialization formats took off together with managed memory languages.
Text formats are popular because you don't have to use a special tool to view them. That's the only reason.
This is easy to write off as micro-optimizing, but it's no joke. I recently profiled some hard real-time software and was surprised to find that >50% of busy processor time was spent somewhere in the fcvt() family of functions.
It's fine for the use case in the medium term (we're within deadline and not starving for more cycles) so I'm avoiding the serialization format transition headache, but it did send me down the rabbit hole of floating point conversion techniques for half a day.
There are probably similar issues in other programming languages, this is not to rag on Java; my point is that when you don't need the 'fancy' floating point formatting stuff, and you need high performance (in my case, I had to convert many billions of floating-points-as-strings and the conversion took ~50% of my program's runtime, which was measured in days), it can pay off to write a custom version of something as seemingly mundane as converting a string to a number.
The standard library functions occupy an awkward middle ground between this type of much faster, slightly sloppy float-to-string converter, and direct storage of the binary representation; in most applications where float serialization performance is actually relevant, at least one of these two alternatives is better.
What about numbers with a large order of magnitude? e.g 1e234. Is making very long strings still faster than switching to e notation?
This won't account for endianness.
Personally, I consider binary formats to be just as readable (from the hexdump, or sometimes even the ASCII directly) and in some ways even easier to parse without ambiguity --- it just requires a bit (pun intended) of learning, like any language. I've worked with someone who could "read" TCP/IP; and I can read much of Z80 and some x86 Asm as well as a few other binary formats.
Somewhat annoying, but might avoid a lot of confusion ("Is floating point math broken?", https://stackoverflow.com/questions/588004/is-floating-point... ).
You're probably going to do something stupid like showing the user a comma-separated list of numbers at some point, which will be needlessly hard to parse for a human when your numbers use a comma as a decimal separator. You (or someone else, or your users if they are technical) will probably at some point make something which tries to parse some output, and that will break if you switch between points and commas arbitrarily. Your users will want to copy a number your software prints and paste it into a calculator or REPL or something, and that probably doesn't work with comma as a decimal separator.
Half-assed "localization" from people who don't know anything about how other countries work is just needlessly annoying to be subjected to.
That's at least my perspective as a Norwegian who experiences a lot of _bad_ localization even though I know English fairly well and configure all my computing devices to use English. The perspective of someone from a country where English is less well known might be different.
<rant>
Examples of horrible localization from clueless American companies or organizations include:
* A lot of software will use your IP address to determine your language. That's annoying when I'm in Norway and want my computers to use English, but it's horrible when abroad. No, Google, I don't want French text just because I'm staying in France for a bit.
* Software will translate error messages, but not provide an error code. All information about error messages online is in English on stackoverflow or whatever. If Debian prints an error message in Norwegian, there's absolutely no information about the error anywhere on the web.
* There was a trend for a while where websites would tick the "localization" checkbox by adding a Google Translate widget, so English websites would automatically translate themselves into completely broken Norwegian automatically. That would've been useless if I didn't know English, and it's even worse considering I already know the source language just as well as Norwegian. Luckily, most websites seem to have stopped doing that.
</rant>
Resist the temptation to use commas or dots as thousand-separators. Seeing a number with a dot as a decimal separator instead of a comma will be fine for most people (even if proper localisation would mean using a comma), but if you throw in commas that mean something else you WILL confuse people. And I imagine the inverse is also true.
These algorithms (Grisu, Ryu, ...) all satisfy the "internal identity requirement", which means that they can print a double and read it back in to the same double.
The harder part is to also produce the shortest of all possible string-representations. (And then picking the closest if there are many).
When considering the correctness of a floating point decimal printing algorithm, you could use "round trips with Y parsing algorithm", but that's flawed -- it ties the printing algorithm directly to a particular parsing algorithm.
Instead what is usually considered is "what is the closest representable floating point value to the real value output by the printing algorithm?" If the printing algorithm outputs a string representation which is closer to the input float than it is to any other float, then it's correct. It's up to each parsing algorithm to provide the reverse condition -- that it parses each string to the float which is closest to the real value of the represented string.
Modern float-print algorithms like Grisu / Dragon / etc. also add the additional restriction that they output the shortest possible string that meets that condition; for example, the real number 0.69999998 is closer to the 32-bit float (0b1.011_0011_0011_0011_0011_0011 * 2^-1, represented in memory as 0x3f333333) than any other float. The real number 0.7 is slightly further from that float, but it's still closer to it than any other float, and is much shorter -- those algorithms should print that float as "0.7".
A correct parser should parse both the string "0.7" and "0.69999998" to the same 32-bit float result.
If round tripping is important, my recommendation would be to output something that directly corresponds to the binary representation of the float. For example, printf %a