od: match GNU's float formatting for -t f2/f4/f8 - #13615
Conversation
|
but comment 0 large, please edit the comment just say a summary and put fixes #<ISSUE_ID> |
|
GNU testsuite comparison: |
nice |
7238f14 to
3840c00
Compare
Merging this PR will degrade performance by 8.71%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | df_with_path |
245.6 µs | 306.9 µs | -19.97% |
| ⚡ | Simulation | du_all_wide_tree[(5000, 500)] |
16.9 ms | 16.2 ms | +4.15% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing HoseungChoi51:od-float-gnu-formatting (3840c00) with main (06f5f2b)
Footnotes
-
46 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
GNU renders a float as the shortest decimal that round-trips, laid out by printf's %g rules. We printed a fixed number of significant digits and switched to scientific notation at the first negative exponent, so ordinary values came out wrong: `od -t f4` gave 1.0000000 for 1.0 and 9.9999998e-3 for 0.01, where GNU prints 1 and 0.01. Four things differed: trailing zeros were kept, the fixed/scientific cut-off was far too low, exponents were not padded to two digits, and NaN was spelled NaN without its sign instead of nan/-nan. Printing a fixed digit count also exposed representation error GNU never shows, rendering 1e-05 as 9.9999997e-6 and 1e38 as 9.9999997e+37. Replace the per-width formatters with one %g-style routine shared by every width. It prints the fewest significant digits that reproduce the value, and chooses fixed or scientific notation as %g does, using at least FLT_DIG/DBL_DIG digits for that choice so a float 1e5 stays 100000 while 1e6 becomes 1e+06. The digit count is the smallest precision whose *correctly rounded* decimal round-trips, which is not always the length of the shortest round-tripping form: for the f32 nearest 2^-96, Rust writes eight digits as 1.2621775e-29, but %.8g rounds to 1.2621774e-29, which reads back as a different float, so GNU prints nine. Rust's form seeds the search as a lower bound and the precision is confirmed from there. Subnormals and the half precision types no longer need special handling: subnormals yield a short digit count on their own, and f16/bf16 widen to float losslessly, so the separate trailing-zero trimming step that fixed -tfH while leaving -tfF wrong is gone. Verified against GNU coreutils 9.11 over all 65536 half and all 65536 bfloat16 bit patterns, 5M random floats, 2.5M random doubles and ~1.1M structured values, with no differences; GNU's own tests/od/od-float.sh fails on main and passes with this change. Derived by black-box comparison of the gnu* binaries across coreutils 8.30, 8.32, 9.4, 9.7 and 9.11; no GNU source was consulted. Confirming that a rendering round-trips costs a format and a parse per value, so this is slower than before -- 2.61s against 1.85s on 20MB for -t f4 -- but still ahead of GNU's 3.26s.
3840c00 to
2ee6160
Compare
| /// The fewest significant digits whose correctly rounded decimal reproduces | ||
| /// `value` exactly — the precision GNU renders at. | ||
| /// | ||
| /// Rust's shortest form is only a *lower bound* here, not the answer. Rust may | ||
| /// pick any decimal in the value's rounding interval, whereas `%g` always emits | ||
| /// the correctly rounded one for a given precision, and that one occasionally | ||
| /// fails to round-trip where a neighbor would. The f32 nearest 2^-96 is such a | ||
| /// case: Rust writes it in eight digits as 1.2621775e-29, but `%.8g` rounds to | ||
| /// 1.2621774e-29, which reads back as a different float, so GNU needs nine. |
There was a problem hiding this comment.
please make this comment shorter
| return format!("{f:width$e}"); | ||
| /// Render `value` the way GNU `od` does. | ||
| /// | ||
| /// GNU prints the shortest decimal representation that round-trips, laid out |
| let exponent: i32 = exponent.parse().expect("`{:e}` emits a decimal exponent"); | ||
|
|
||
| let precision = digits.max(kind.min_digits()) as i32; | ||
| if exponent < -4 || exponent >= precision { |
There was a problem hiding this comment.
This is %g again - uucore's format_float_shortest has the same -4/precision cut-off, trailing-zero strip and e+06 padding.
| /// suffice to round-trip, so e.g. a `float` 1e5 stays `100000` while 1e6 | ||
| /// becomes `1e+06`. | ||
| fn format_float(value: f64, kind: FloatKind) -> String { | ||
| if value.is_nan() { |
There was a problem hiding this comment.
ExtendedBigDecimal::from(f64) already maps nan/-nan/inf/-inf/-0, so these branches would go away too.
| if force_exp { | ||
| return format_f64_exp_precision(value, width, precision - 1); | ||
| /// Drop trailing fractional zeros, and the decimal point if nothing follows it. | ||
| fn strip_trailing_zeros(s: &str) -> String { |
There was a problem hiding this comment.
Duplicate of strip_fractional_zeroes_and_dot in num_format.rs.
| let bits = value.to_bits(); | ||
| (bits & 0x7C00) == 0 && (bits & 0x03FF) != 0 | ||
| /// Right-align a rendered value in the column width `od` reserves for it. | ||
| fn pad(repr: &str, width: usize) -> String { |
There was a problem hiding this comment.
num_format::Float does this with width + NumberAlignment::RightSpace.
| /// fails to round-trip where a neighbor would. The f32 nearest 2^-96 is such a | ||
| /// case: Rust writes it in eight digits as 1.2621775e-29, but `%.8g` rounds to | ||
| /// 1.2621774e-29, which reads back as a different float, so GNU needs nine. | ||
| fn significant_digits(value: f64, kind: FloatKind) -> usize { |
There was a problem hiding this comment.
This is the actual fix and worth keeping - it just needs to feed precision into uucore's formatter.
| /// Half precision values (`-t f2`, `fH` and `fB`) are widened to `float`, which | ||
| /// is lossless, and share its formatting. | ||
| #[derive(Clone, Copy)] | ||
| enum FloatKind { |
There was a problem hiding this comment.
Four two-arm matches for two constants; passing the two digit counts around would read lighter.
| format!(" {}", format_long_double(f)) | ||
| } | ||
|
|
||
| fn format_long_double(f: f64) -> String { |
There was a problem hiding this comment.
This one still prints NaN and a bare 1e21, so the file now has two float printers that disagree. Same treatment, or a comment on why it's out of scope? It also has no tests.
Fixes #13608. Supersedes #13610.
GNU
odprints a float as the shortest decimal that round-trips, laid out by%g's rules. We printed a fixed digit count and switched to scientific notation far too early, sood -t f4gave1.0000000for 1.0 and9.9999998e-3for 0.01.Replaces the per-width formatters with one
%g-style routine shared by all of them; GNU'stests/od/od-float.shnow passes. Rationale and testing are in the commit message.Written with AI assistance; no GNU source was consulted.