Extend float-to-string itoa fast path for large integer values#100649
Conversation
The previous implementation of `DecomposedFloat::isIntegerInRepresentableRange` only recognized integers with normalizedExponent <= mantissa_bits (23 for Float32, 52 for Float64). This missed all integers with higher exponents, where all mantissa bits contribute to the integer part and no fractional bits exist. For Float32, this meant values like 1e9f (exponent 29) were not recognized as integers, falling through to the slower float-to-string path instead of the fast `itoa` path in `writeFloatTextFastPath`. This should show as a performance improvement in the `float_formatting` performance test for Float32. The fix adds the missing case: when exp > mantissa_bits, the value is always an integer — we only need to check it fits in the target signed integer type (Int32 for Float32, Int64 for Float64) via the exp < bits-1 bound. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Workflow [PR], commit [75d4429] Summary: ❌
AI ReviewSummaryThis PR extends the float-to-string integer fast path for ClickHouse Rules
Final Verdict
|
|
Failures are a curious: 2^55 was output as |
For Float32 values with normalizedExponent > 24 (roughly 33M to 2B), the ULP exceeds 1, so multiple integers map to the same float. A plain Int32 cast gives the exact value, but shortest-representation algorithms like dragonbox prefer "rounder" decimals (more trailing zeros). This adds a rounding step that finds the roundest integer within the valid dragonbox range, matching dragonbox output exactly for all 2.1 billion positive Float32 values (verified exhaustively). The rounding uses hardcoded % 10 / % 100 / % 1000 with compile-time constant divisors (imul+shift, no hardware div). IEEE 754 boundary tie-breaking (inclusive for even mantissa, exclusive for odd) is folded into the half_ulp values, eliminating branches. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix stale comments after multiple iterations: separate the general explanation from the function documentation, correct function name references, and clarify the exhaustive verification claim. Simplify the itoa condition in `writeFloatTextFastPath` to use `isIntegerInRepresentableRange` instead of manual mantissa bit masking. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same approach as the Float32 rounding: for Float64 values with normalizedExponent 54-62 (roughly 1.8e16 to 9.2e18), find the "roundest" integer within the valid dragonbox range using compile-time constant modulo operations (% 10, % 100, % 1000, % 10000). Also adds exp=53 (ULP=2) to the plain itoa path, same as Float32 exp=24. Verified: exhaustive check of all 2.1B positive Float32 values, plus 1 billion random Float64 samples (100M per exponent) and all power-of-2 boundary values — zero mismatches with dragonbox. Add a performance test for Float64 large integers starting at 2^53. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
isIntegerInRepresentableRange to cover large float integersitoa fast path for large integer values
|
Adjusted the new covered range with rounding equivalent to dragonbox so the output is exactly the same, but the performance is several times faster. It should be visible in existing performance test |
- Fix negative value handling in the rounding path: the modulo logic assumed positive values. For negative floats (e.g. BFloat16, negative Float64), we now write the sign first, then round the absolute value. Fixes 03366_bfloat16_sorting, 03367_bfloat16_tuple_final, and 01177_group_array_moving test failures. - Update verification comments to reflect actual testing coverage: Float32: all ~4.3B values exhaustively (positive and negative). Float64: 10B random samples (500M per exponent × 10 × pos/neg) plus all power-of-2 boundaries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The random sampling of queries makes it harder to analyze, but it seems some cases have regressed while others where noticeable (4x!) improved: |
|
If you want to give a look to even more interesting ways to improve math operations look at fastermath maybe some tricks there could be useful. |
…resentableRange-float32
…ed branch - Replace TRY_ROUND_TO_SHORTEST macro with `tryRoundToShortest` template function for better type safety and debuggability. - Add NO_INLINE to both rounding functions to prevent the compiler from inlining them into the hot dispatch path (was causing a 10-15% Float64 regression from icache pressure — 1679 bytes inlined vs 284 bytes out-of-line). - Use unsigned types (UInt32/UInt64) for half_ulp, lo, hi since they are always positive after sign handling. - Collapse the two dependent branches in the pick logic into a single `prefer_up` expression, reducing instructions by 12-15%. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
I've also applied |
Add a stateless correctness test for the extended itoa fast path covering Float32 exp 24-30 and Float64 exp 53-62: power-of-2 boundaries, their neighbors, negative values, and BFloat16. Fix a typo in a comment referencing `writeFloatTextFastPathRounded` instead of `writeFloatTextFastPathFloat32Rounded`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LLVM Coverage Report
Changed lines: 100.00% (85/85) · Uncovered code |
|
Looks good now: |
Ergus
left a comment
There was a problem hiding this comment.
It is fine, just commented to say something ;)
| if (shift >= 10 && tryRoundToShortest<10000>(v, lo, hi, buffer, start, result)) return result; | ||
| if (shift >= 7 && tryRoundToShortest<1000>(v, lo, hi, buffer, start, result)) return result; | ||
| if (shift >= 3 && tryRoundToShortest<100>(v, lo, hi, buffer, start, result)) return result; | ||
| if (tryRoundToShortest<10>(v, lo, hi, buffer, start, result)) return result; |
There was a problem hiding this comment.
For short type names I am not a fan of auto, makes the code a bit more obscure for a reader... but that's life.
e6493b7
The test sorts a hand-built `BFloat16` array that contains five values which all round to the same `BFloat16` representation (`+0.0`, `+0.0`, `0.0`, `-0.0`, `0.0`). Equal sort keys have an implementation-defined relative order; under enough query-plan randomization the relative order of `-0` against the surrounding `0`s changes, and the result no longer matches the byte-exact `.reference` file. Pinning `max_threads = 1` makes the order produced by `MergeSortingTransform` deterministic and matches the committed reference. The test still exercises sorting of `BFloat16` values across the full input. Diagnosed by @alexey-milovidov on ClickHouse#93830 (comment) CI history: 4 hits across 4 unrelated PRs in 90 days (ClickHouse#93830, ClickHouse#100983, ClickHouse#100649, ClickHouse#96130), 0 master failures, 78957 OK runs / 1 FAIL in 30 days.

Extend the
itoafast path inwriteFloatTextFastPathfor floating-point values that are integers but whose exponent exceeds the mantissa width (ULP > 1). In this range, multiple integers map to the same float, and shortest-representation algorithms like dragonbox prefer "rounder" decimals (more trailing zeros).What changed:
For Float32 exp 24 (ULP=2) and Float64 exp 53 (ULP=2): all values are even integers, plain
itoamatches dragonbox — added directly to the fast path.For Float32 exp 25-30 (ULP 4..128): a rounding step finds the "roundest" integer within the valid dragonbox range by trying
% 1000,% 100,% 10from largest to smallest.For Float64 exp 54-62 (ULP 4..1024): same rounding approach, with an additional
% 10000step for exp 62 (ULP=1024).All modulo operations use compile-time constants (no hardware
div). The IEEE 754 boundary conditions (power-of-2 asymmetry, even/odd mantissa tie-breaking) are handled correctly.Negative values are handled by writing the sign first, then rounding the absolute value.
Added a performance test for Float64 large integers starting at 2^53.
Range covered:
itoa)itoa)itoa)itoa)Verification:
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Faster float-to-string conversion for large integer values by extending the
itoafast path with dragonbox-compatible rounding.Documentation entry for user-facing changes
Version info
26.4.1.487