-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Default float formatting is too narrow-minded #24556
Comments
FYI, I'm already working on the flt2dec patch (but was a bit busy for recent weeks). I have all necessary tools for formatting, including precision customization (see |
I think this is a good idea for the default float->string behavior -- it has better interoperability properties. I opened issue #1075 for it in the rfcs repo. |
Presumably saying I just suggested in a comment on #1074 that we might want to consider adding a format modifier |
I suggest we use Grisu2 for Debug printing f32/f64. fn main() {
let a = 0.57 * 100f64;
let b = 57f64;
assert_eq!(format!("{:?}", a), format!("{:?}", b));
assert!(a != b)
} |
To be clear, the PR adds Grisu3 + Dragon4, not Grisu2. That is, it always finds the shortest representation at the cost of more code (and needing significantly more time and memory on about 0.5% of all possible floats). In the issue, I promoted Grisu2 in an attempt to keep implementation effort a bit lower, not knowing that @lifthrasiir already solved all problems and nobody worries about the extra code (and some of that code is necessary for future float parsing work anyway). tl;dr Forget about Grisu2 and celebrate Grisu3! |
How does Grisu3 + Dragon4 compare to Burger & Dybvig? This is basically the same thing as David Gay's algorithm, but was developed separately, and apparently differs slightly. More importantly, David Gay's C implementation is "mind-numblingly complex" and was written to prioritize speed over everything else, but the algorithm described in that paper is apparently significantly simpler (and presumably performs well, just isn't as fast as David Gay's C implementation). The paper also includes a pretty simple Scheme code implementation (of the floating-point algorithm; it describes changes to the algorithm for fixed-point high-precision that is faster but doesn't give Scheme code for it) Quoted from this python-list email:
So David Gay's algorithm is generally faster than Burger & Dybvig but I guess is too complex to be worth reimplementing. But this doesn't tell me how Burger & Dybvig's algorithm compares to Grisu3 + Dragon4 (both of which I know nothing at all about). I'm also not sure offhand whether Burger & Dybvig's algorithm requires heap allocation. |
(And of course, how does it compare to Grisu2?) |
Dragon4 is the algorithm of Burder and Dybvig. I didn't find the name in the paper but it's stated elsewhere that this is a semi-official name, even attributed to the authors. As for comparisons: Grisu3 is very clever, simple and fast. Read the paper, its notation is a mess but it's enlightening nevertheless. |
I think others already have explained the situation well, but to recap:
[1] A variant of this table is briefly mentioned in the Grisu paper: "We will base future evolutions of Grisu on Performance-wise, Grisu2 is the best, with Grisu3 + Dragon4 closely following. Dragon4 is much slower but has a simpler and more verifiable implementation (both glibc and msvcrt uses it). The merit of Space-wise, all algorithm can be implemented without dynamic memory allocation. The bignum is limited in size so a fixed-size array is enough (and this is how I've implemented Dragon4). The lookup table size of 1K in Grisus might seem a lot, but this is easily overshadowed by binary size (in my PR, about 30K). |
This is a direct port of my prior work on the float formatting. The detailed description is available [here](https://github.com/lifthrasiir/rust-strconv#flt2dec). In brief, * This adds a new hidden module `core::num::flt2dec` for testing from `libcoretest`. Why is it in `core::num` instead of `core::fmt`? Because I envision that the table used by `flt2dec` is directly applicable to `dec2flt` (cf. #24557) as well, which exceeds the realm of "formatting". * This contains both Dragon4 algorithm (exact, complete but slow) and Grisu3 algorithm (exact, fast but incomplete). * The code is accompanied with a large amount of self-tests and some exhaustive tests. In particular, `libcoretest` gets a new dependency on `librand`. For the external interface it relies on the existing test suite. * It is known that, in the best case, the entire formatting code has about 30 KBs of binary overhead (judged from strconv experiments). Not too bad but there might be a potential room for improvements. This is rather large code. I did my best to comment and annotate the code, but you have been warned. For the maximal availability the original code was licensed in CC0, but I've also dual-licensed it in MIT/Apache as well so there should be no licensing concern. This is [breaking-change] as it changes the float output slightly (and it also affects the casing of `inf` and `nan`). I hope this is not a big deal though :) Fixes #7030, #18038 and #24556. Also related to #6220 and #20870. ## Known Issues - [x] I've yet to finish `make check-stage1`. It does pass main test suites including `run-pass` but there might be some unknown edges on the doctests. - [ ] Figure out how this PR affects rustc. - [ ] Determine which internal routine is mapped to the formatting specifier. Depending on the decision, some internal routine can be safely removed (for instance, currently `to_shortest_str` is unused).
Now fixed by #24612. |
Closed by #24612. |
Currently,
{}
and{:?}
always prints at most 6 digits after the decimal point, and never uses scientific exponential notation (1.23e6
) regardless of how large the number is.This has several undesirable consequences:
0.1 + 0.1 + 0.1
is not the same float as0.3
, but the default option does not reflect that.Debug
specifically, it hides important differences (like the above0.1 + 0.1 + 0.1
vs0.3
) which can mislead even experienced programmers when debugging, or at least force them to use unsightly formatting codes like{.17}
(which is harder to read).There are smarter algorithms that do better on all accounts (and also fix the accuracy issues reported in #24557). See e.g. Python 3.1+ (last bullet point, also back-ported to 2.7). It would be a great boon for people whose debugging consists of staring at the results of float calculations if Rust did the same thing.
These algorithms search for, roughly, the shortest string that reproduces the number exactly (bit for bit identical) when read back with an accurate parser. They also use scientific exponential notation when appropriate. We probably don't have accurate parsing either (again, see #24557) but this issue is not about that.
I propose adopting such an algorithm. This would lead to the following differences:
{:.17}
(this makes round trip-safe outputs easier to eyeball).The combination of these changes mean a good balance between accuracy and not overly burdening readers.
Debug
andDisplay
can still differ on minor details like whether negative zero is printed with a minus sign (#20596), but the changes listed above should apply to both. Exponential scientific notation is by no means exclusive to programmers, it's used by many calculators (hardware and software) for example, and printing a hundred digits is hardly very user friendly either.Implementation
Python uses Martin Gay's algorithm (and his C implementation of the same), which by all accounts is incredibly complicated and complex --- porting it won't be fun. It also needs memory allocation, which disqualifies it from
core
(I'm sure there is an upper bound on how many bits it needs, but identifying that bound would be yet another porting hurdle).Florian Loitsch's Grisu algorithm(s) should be doable (with the caveat that I'm only halfway through the paper myself). Grisu3 "gives up" on about 0.5% of all possible floats, but IIUC Grisu and Grisu2 can handle those, they just doesn't guarantee finding the shorted possible string, an acceptable trade off IMHO. I'm not sure whether using only Grisu2 gives the same result as Grisu3 on the floats the latter does handle, but if so, that would simplify the implementation.
There is an existing implementation (that uses only
core
) of Grisu3 in rust-strconv by @lifthrasiir --- anyone interested in working on this should get in touch with the author. I'm not sure if we'd want to import that implementation wholesale (even assuming the author's cooperation) though: It falls back to Dragon4 for the numbers Grisu3 can't handle, so it's rather more code (and more complicated) than the Grisu2-only option.There are some open question though: How should formatting options like
.N
andLowerExp
andUpperExp
be handled? Can Grisu handle these, or do we need to build on top of it? I don't care that much about perfect accuracy for these (since they round anyway), so this may be easier than we expect.cc @rprichard
The text was updated successfully, but these errors were encountered: