Repository navigation
Accumulate number digits while scanning them - #5851
Merged
Merged
Conversation
nlohmann
added this pull request to stack #5856
October 15, 2026 17:44
nlohmann
marked this pull request as draft
October 16, 2026 05:19
nlohmann
force-pushed
the
perf/lexer-number-scan
branch
from
October 17, 2026 06:03
fe8900b to
a31b539
Compare
nlohmann
force-pushed
the
perf/lexer-number-scan
branch
from
October 17, 2026 06:30
a31b539 to
722b2c4
Compare
Base automatically changed from
claude/parser-stream-performance-8cebe9
to
develop
October 19, 2026 20:01
The contiguous number fast path validated a token byte by byte, copied it into token_buffer, and then converted it in out-of-line functions that scanned the digits a second time: parse_float_native() called parse_float_significand(), which returned its result through memory and called std::any_of (out of line with GCC) twice per number, and parse_integer_signed() divided by ten on every digit for its overflow check. scan_number_bulk_contiguous() now accumulates the significand while it validates the digits, eight at a time where at least eight bytes remain (is_eight_digits() and parse_eight_digits(), after fast_float). Floats with at most 19 digits go straight to decimal_to_float(); integers with at most 18 digits are stored without an overflow check (a narrower number type still rejects values that do not round-trip). Longer tokens take the existing path. has_nonzero_digit() is a plain loop and parse_float_significand() is always inlined, which helps that path. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The section compares error messages from both lexer paths and parses out-of-range numbers, so it does not compile (the caught exception does not exist) and would abort with JSON_NOEXCEPTION. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
force-pushed
the
perf/lexer-number-scan
branch
from
October 20, 2026 05:51
722b2c4 to
c4c925a
Compare
gregmarr
approved these changes
Oct 20, 2026
| { | ||
| return c != '0'; | ||
| }); | ||
| w = (w * 10u) + static_cast<std::uint64_t>(data[i] - '0'); |
Contributor
There was a problem hiding this comment.
Is this repeated code that could use a helper?
Owner
Author
There was a problem hiding this comment.
Yes, though not quite this loop: the integer part and the fraction in parse_float_significand() ran the same eight-digits-then-one-digit loop. In 2dfda27 they share accumulate_significant_digits(), and clang -O2 emits the same code as before. accumulate_digits() stays separate because it checks each byte for a digit and has no 19-digit cap, so a shared helper would need both as parameters.
— posted by Claude Code on behalf of @nlohmann
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
marked this pull request as ready for review
October 21, 2026 10:26
nlohmann
added a commit
that referenced
this pull request
Oct 21, 2026
The Visual Studio 2019 job (Release, x86, JSON_ImplicitConversions=OFF) builds the tests for all standards and takes 49 to 57 minutes when it passes, close to AppVeyor's 60-minute limit per job. In build 54917118 (#5867), build 54917312 (#5620), and build 54930878 (#5851) it was stopped at the limit while still compiling. Split it with JSON_TestShard like the Visual Studio 2017 jobs. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Parsing numbers from contiguous input scanned every digit twice.
scan_number_bulk_contiguous()validated the token one byte at a time and copied it intotoken_buffer. The conversion then went through two out-of-line functions that read the digits again:parse_float_native()calledparse_float_significand(), which returned its result through memory and calledstd::any_oftwice per number (GCC does not inline it).parse_integer_signed()divided by ten on every digit for its overflow check.This PR accumulates the value while the digits are validated:
is_eight_digits()tests them andparse_eight_digits()converts them, as in fast_float.decimal_to_float()(Clinger's fast path or Eisel–Lemire), so the float fast path has no calls left.number_integer_t/number_unsigned_tstill rejects values that do not round-trip, as before.long double), take the existing path. It is unchanged, except thathas_nonzero_digit()is now a plain loop andparse_float_significand()is always inlined.The stream windows of #5828 are handled like the existing fast path: a number that reaches the end of a window falls back to
scan_number(), and the terminator is read and ungotten.This is the first of five stacked PRs from an assembly review of the number and string parsing; the stack is based on #5828.
Performance
Change of the best-of-runs time against the previous PR (the first: against #5828), negative is faster. accept is
json::accept()(no allocations), parse isjson::parse(). Apple M1 Max: Apple Clang 21 and Homebrew GCC 16; x86-64 (Haswell VM): GCC 13, and Clang 18 with-falign-loops=64 -falign-functions=64on both sides (without it, code placement alone moves Clang's numbers by up to ±5 % between builds). Improvements of 3 % or more are bold.canada.jsoncitm_catalog.jsontwitter.jsonnumbers.jsonlong_strings.jsontwitterescaped.jsonmesh.jsonThis PR does not touch the string code; the x86-64 GCC rows for twitter.json and twitterescaped.json (+5 to +6 %) come from code placement and from the shared machine, which was under load during this run (the M1 rows for these files are within ±1 %).
Tests
unit-class_lexer.cpp:std::list<char>: since Make parsing from std::istream and FILE* fast #5828, astd::stringstreamtakes the bulk path as well, so it is no longer an independent reference.basic_jsonwith 32-bit integers checks the round-trip check.accept()results and SAX events (including the float text), for 737 files of json_test_data and 500k generated inputs. Each input is parsed from a string, anstd::istreamand aFILE*. Run on Apple M1 (Clang) and x86-64 (GCC 13, Clang 18).Breaking changes
None. Only internal functions in
nlohmann::detailchange. Values, exceptions and error positions are the same, as the differential check above shows.🤖 Generated with Claude Code