6 ms·
Back in 2007-ish I spent a bunch of time trying to figure out how to print floating point numbers losslessly. Annoyingly, there's no printf() format string that
by kentonv 8y ago
Back in 2007-ish I spent a bunch of time trying to figure out how to print floating point numbers losslessly. Annoyingly, there's no printf() format string that says "print exactly as many digits as needed so that it parses back to the same value". You can specify a number of digits to use, but to avoid printing unnecessary digits in all cases (e.g. "0.20000000000000001" for 0.2) you must ask for a maximum of 15 digits, but in some cases you will lose data if you ask for fewer than 17 digits.
After asking around, I came across the Steele & White paper, and the implementation known as `dtoa()` written by David M. Gay. At the time, this seemed to be understood to be the "correct" answer.
But then I looked at the code: http://www.netlib.org/fp/dtoa.c http://www.netlib.org/fp/dtoa.c
Uh.
There's a lot to dislike about that code, but arguably the worst thing is that it isn't thread-agnostic. It mutates global variables and protects them with a global mutex. A global mutex lock, just to print a number! Whyyyyyyyy?
So then I tried something different: I wrote some code that would do sprintf() with 15 digits precision first, then parse it with strtod() to see if it came back exact. If not... then I did sprintf() with 17 digits precision... and called it a day.
In benchmarks, this turned out to be just as fast as calling dtoa().
And so that's how the Protocol Buffers library deals with numbers when writing TextFormat:
https://github.com/google/protobuf/blob/ed4321d1cb33199984118d801956822842771e7e/src/google/protobuf/stubs/strutil.cc#L1174-L1213 https://github.com/google/protobuf/blob/ed4321d1cb3319998411...
But the bigger lesson for me was: Transmitting floating-point numbers in text is awful. People have no idea how ridiculously complex this is, because it seems like it ought to be simple. When you send JSON with numbers in it, you are probably invoking code that looks something like dtoa(), over and over and over again. And that's just to write them out; I have no idea how complex the parsing side is.
Please, folks, think of the CPU cycles. When sending numeric data, use a binary format.
- taneq 8y ago> When sending numeric data, use a binary format. Binary representations of floating point number are a bit of a nest of vipers too, though. You can probably just pipe IEEE floats across the network as bytes, in practice, but it's risky. It's probably safer in many cases to just transmit things in fixed point.
- dbaupp 8y agoI can think of a few possible problems with IEEE754-across-the-wire, such as: - not all hardware supports them - subnormal numbers are annoying - NaNs can be tricky to handle, such as signalling and (if payloads are used) security risks through NaN payloads. Which are you thinking of? Or something else?
- jcelerier 8y ago> I can think of a few possible problems with IEEE754-across-the-wire, such as: > - not all hardware supports them let's be reasonable : how many people who run architectures which don't have any float would actually send floats to them over the network ?
- gnufx 8y agoThe parent didn't say "don't have any float". I have had to translate between little endian IEEE754 and big endian IBM format, roughly copying what HDF did. Protein crystallographers invented their own binary file format rather than just using HDF, and didn't define the binary floating point format, so that the files weren't portable initially.
- taneq 8y agoI was mainly thinking of byte order issues and handling of NaN / infinities (especially if, say, you're sending a value from one computer with FPU exceptions disabled to another one with exceptions enabled), but I seem to recall reading about other subtle implementation differences which tended to mean that floats aren't always portable between different CPUs even if they're ostensibly all IEEE compliant.
- kentonv 8y agoAlmost everything uses IEEE-754. For those that don't, converting IEEE-754 to the local native format is almost certainly much easier than parsing text. Dealing with NaN-related issues is also only a couple instructions; much easier than parsing text. (Ideally, use a serialization library that already does these things.)
- pjc50 8y ago"%0.17g" is the format string I eventually concluded was the preferred one for this.
- kentonv 8y agoUnfortunately, that format will print 0.2 as "0.20000000000000001". What we want is the shortest-length string that will parse back to the same original value, which is "0.2".
- deleted 8y ago[deleted]
- CamperBob2 8y agoPlease, folks, think of the CPU cycles. When sending numeric data, use a binary format. You were making so much sense up until that point. :(
- gnufx 8y agoPeople often forget about the numeric locale with floating point as text. Long ago, we had European collaborations, e.g. with Anglophone at one end of the network, Francophone or Danish at the other, sometimes "with hilarious consequences". I know %g etc. in C's printf isn't subject to that, but it was a real issue at the time.
- kentonv 8y agoYeah, my sprintf()-based code includes some ridiculous hacks to de-localize the output. :(