Skip to content

Parse a long input once instead of twice - #411

Open
Algunenano wants to merge 1 commit into
fastfloat:mainfrom
Algunenano:long-input-single-parse
Open

Algunenano wants to merge 1 commit into
fastfloat:mainfrom
Algunenano:long-input-single-parse

Conversation

@Algunenano

@Algunenano Algunenano commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

A mantissa of more than 19 significant digits needs more than 19 characters, so only a longer input can come back too_many_digits. Today that is discovered after a full parse without spans, and the input is then parsed a second time with them, in parse_number_slow_path. Testing the length up front sends it straight to the parse that materializes the spans, so it is parsed once.

This comes out of a round of profiling from_chars in ClickHouse, which parses floats from text for every row of a text format. We have a local helper that does this, parsing once with the spans materialized for inputs known to be long, though our own dispatch reaches it rarely. Of the float-parsing changes we ended up carrying, this is the one that also helps the library's own benchmark datasets, which is why it is the one being proposed.

The change is eight lines and does not touch the common path: the length test is under fastfloat_unlikely, and the parse below still gets the literal store_spans = false it had, which is what lets the force-inlined parser drop the span stores entirely.

Measurements

benchmarks/benchmark.cpp on an Intel Xeon 6975P-C (Granite Rapids) with clang 22.1.8, cycles per float, best of 5. canada and mesh are the repository's own datasets, the rest are 50k-line sets chosen to hit one path each.

canada mesh round-trip doubles short decimals small integers
before 47.30 26.20 43.94 31.09 27.00
after 46.90 25.68 43.84 31.90 26.93
long fractions 20-38 digit integers >38 digit integers long leading zeros
before 233.94 259.73 447.73 125.21
after 195.68 181.65 273.11 126.34

Results are bit-identical to main (value, errc and consumed length, for both double and float) over 400k adversarial inputs: 1 to 84 digit integers, long fractions, leading zeros, explicit exponents, and %.17g of random bit patterns. ctest passes with clang 22 and with gcc in C++20 mode.

The trade-off

The test looks at the range, not at the number. A caller that hands from_chars the whole remaining buffer for a short value will take the slow path on every value: measured at 20% for 7-character numbers in a large buffer (9.31 to 11.08 ns per value), against 6% faster when the range is tight (8.09 to 7.59 ns).

I tried deciding on the token instead, with a probe loop that counts mantissa characters one at a time, up to twenty, before committing. It costs more than the second parse it avoids: long fractions went to 258 cycles per float, worse than doing nothing. That is a result about that particular probe, though, not about token classification in general. ClickHouse classifies the token as well, but with a SWAR scan that consumes eight digits per iteration and bails out as soon as it sees ., e or E, and there it pays for itself. I have not tried a SWAR probe here, so this trade-off may be removable. Happy to do that, or to drop or gate the change, whichever you prefer.

Two fast paths ClickHouse keeps but that do not belong here

ClickHouse has two more float-parsing fast paths that I am deliberately not proposing. They work there because its dispatch already knows the token length and routes by shape, so each one only runs on the inputs it suits. Inside the library the choice would have to be made blind, and then each one wins on one shape and loses on the rest:

  • An exact unsigned __int128 path for plain integers of at most 38 digits. 27% better on exactly that shape, 7 to 16% worse on every other long input, because of the extra scan needed to recognise it. Placed before digit_comp instead of in front of the parse it does nothing at all, since the mantissa versus mantissa + 1 disambiguation already resolves those inputs.
  • Counting rather than accumulating digits past the nineteenth significant one. 21% better on integers over 38 digits, but splitting the integer scan into two loops costs 13% on mesh and on small integers.

This touches the same function as #410, so whichever lands second needs a trivial rebase.

A mantissa of more than 19 significant digits needs more than 19 characters,
so only a longer input can come back too_many_digits. Today that is discovered
after a full parse without spans, and the input is then parsed a second time
with them. Testing the length up front sends it straight to the parse that
materializes the spans.

benchmarks/benchmark.cpp, Intel Xeon 6975P-C, clang 22.1.8, cycles per float,
best of 5:

                 canada  mesh   long fractions  20-38 digit ints  >38 digit ints
  before          47.30  26.20          233.94            259.73          447.73
  after           46.90  25.68          195.68            181.65          273.11

Results are bit-identical over 400k adversarial inputs.

Caveat: the test looks at the range, not at the number. A caller that passes
the whole remaining buffer for a short value takes the slow path on every
value, measured at 20% for 7-character numbers in a large buffer. Deciding on
the token instead costs more than it saves: a 20-character probe loop is
slower than the second parse it avoids.
@lemire

lemire commented Sep 21, 2026

Copy link
Copy Markdown
Member

@Algunenano Why is ClickHouse parsing these numbers with > 19 digits ? What is the application? What system is producing these outputs?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants