Repository navigation
Conversation
nlohmann
added this pull request to stack #5739
September 30, 2026 13:21
This was referenced Sep 30, 2026
nlohmann
force-pushed
the
json-view/02b-float-parser
branch
from
September 30, 2026 18:06
d92b762 to
39092df
Compare
nlohmann
force-pushed
the
json-view/02b-float-parser
branch
from
September 30, 2026 18:06
39092df to
192b99b
Compare
nlohmann
marked this pull request as ready for review
September 30, 2026 18:15
nlohmann
force-pushed
the
json-view/02b-float-parser
branch
from
September 30, 2026 18:19
192b99b to
9c71689
Compare
gregmarr
reviewed
Oct 1, 2026
| - The library converts integers and floating-point numbers itself, independent of the locale. Floating-point | ||
| numbers are correctly rounded (to nearest, ties to even). Only a `#!c long double` that is not IEEE 754 binary64 | ||
| (e.g., the 80-bit x87 format) is converted with `#!cpp std::from_chars` where available, or with | ||
| [`std::strtold`](https://en.cppreference.com/w/cpp/string/byte/strtof), which gets the decimal point of the |
Contributor
There was a problem hiding this comment.
Don't we convert the decimal point back to period?
Owner
Author
There was a problem hiding this comment.
Yes. The token always holds .. Only the strtold fallback (for a long double that is not binary64) swaps it for the locale's decimal point during the call, and puts . back afterwards. So the result does not depend on the locale. The note read as if the input had to use the locale's decimal point; 9d88ead rewrites it.
This comment was written by Claude Code on behalf of @nlohmann.
Give the library its own correctly rounded float converter for binary32 and binary64 (IEEE 754), and speed up the lexer's string and escape scanning. The converter splits a number token into sign, significand, and decimal exponent, then tries Clinger's fast path, then a templated Eisel-Lemire step, and falls back to an exact big-integer digit comparison for tokens with more than 19 significant digits whose two candidate values round differently. This replaces std::from_chars and strtod/strtof for both formats, so parsed values no longer depend on the C/C++ library or the current locale. The strtold fallback kept for other long double formats (x87, binary128) now also copies a multi-byte decimal point correctly, fixing #5660. eisel_lemire() and decimal_to_float() are always inlined so callers keep the whole conversion in their hot loop. The string-scanning kernels in string_scan.hpp find a stop byte with the trailing-zero count of the SWAR mask instead of a byte loop, and scalar_string_bulk_run() validates a run of multi-byte UTF-8 sequences one after another instead of re-searching after each one. get_codepoint() decodes a contiguous \uXXXX escape with one table lookup per byte instead of four range-checked get() calls; the streaming path and all error positions are unchanged. Adds 508 generated hard float-parsing cases with expected binary32 and binary64 bits, and kernel-comparison tests for the string scans and the escape table against byte-by-byte references. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann
removed this pull request from stack #5739
October 6, 2026 09:26
This was referenced Oct 6, 2026
nlohmann
force-pushed
the
json-view/02b-float-parser
branch
from
October 6, 2026 09:28
15b0cc0 to
953d74d
Compare
nlohmann
added this pull request to stack #5768
October 6, 2026 09:29
This branch has not been deployed
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.
Part of the stack for the zero-copy view (#5295). This change speeds up
json::parseon its own; the view uses the same number converter and string kernels later in the stack. This PR combines #5738, #5618, and #5619, which were reviewed separately before.Summary
float,double, andlong doublewhere it is IEEE 754 binary64 (MSVC, Apple arm64) are now converted by the library itself: correctly rounded (to nearest, ties to even), independent of the locale, and without calling the C or C++ library.w(at most 19 digits), and decimal exponentq. The scanners already record the positions of the decimal point and the exponent, so no character is classified again; digits are read eight at a time.wand10^|q|are exact (skipped whereFLT_EVAL_METHOD != 0).wandw + 1round differently: an exact big-integer comparison with the midpoint between the two candidates (fast_float's digit comparison, simplified because Eisel-Lemire already yields both candidates).std::from_chars/strtod) was correctly rounded. Out-of-range values still throwout_of_range.406, and underflow still gives a zero with the sign of the token.eisel_lemire()anddecimal_to_float()are always inlined, so callers' hot loops keep the whole conversion inline; this makes the view's traversal of canada.json 8% faster later in the stack.strtoldfallback left forlong doubleformats other than binary64 (x87, binary128) now copies a decimal point longer than one byte into a copy of the token instead of substituting its first byte, fixing Floats are truncated at the decimal point under locales with a multi-byte decimal point (fa_IR.UTF-8) #5660 completely for a locale such asfa_IR.UTF-8.string_scan.hppfind the first stop byte with the trailing-zero count of the comparison mask instead of a byte loop once a word contains a hit (the lowest flagged byte is always a real hit, since borrows can only flag bytes above one). Words are assembled in little-endian order on every platform.scalar_string_bulk_run()validates a run of multi-byte UTF-8 sequences one after another instead of re-searching for the next special byte after each sequence, which helps text in non-Latin scripts. These kernels serve the lexer's contiguous fast path, the serializer, and the binary formats.\uescape decoding.get_codepoint()decoded four hex digits with four calls toget(), each going through a chain of range comparisons. For contiguous input it now uses one table lookup per byte (after yyjson'sread_hex_u16); an invalid digit shows in the OR of the four values, and the lexer then skips the four bytes and updates position counters as before. The streaming path and all error positions are unchanged.number_handling.md,template_parameters.md,number_float_t.md) no longer say parsing usesstrtod/strtof/strtold, except for non-binary64long double.README.md/license.mdnow also names the digit comparison.Performance
json_view
The view uses these converters and kernels from group 3 on; it is not affected directly by this PR.
Core library (json::parse / dump)
Measured on the regrouped stack
Measured on the regrouped stack (µs, best of 3 interleaved rounds of 15 runs; files from nativejson-benchmark). Apple M1 Max with Apple clang at
-O2; x86-64 on a KVM Haswell VPS, pinned to one core, GCC 13 / Clang 18 at-O2(the VPS is noisy, about ±5–10%).json::parseandjson::dump, develop → this PR:Earlier measurements (on the old stack)
Apple clang,
-O2, end-to-endjson::parsefrom a string, ns per number (structure included), vs. develop:floatjsonjson::parsein a separate process, best of 5, Apple M1 Max:\utable)tests/benchmarks, median of interleaved repetitions (string scan also speeds updump()):Tests
json::parsewith both scanners.doubleandfloat(ties to even, subnormal/overflow boundaries), a 200,000-value round trip, and a new 100,000-valuefloatround trip.\utable is checked for valid escapes, surrogate pairs, truncation at every distance from the end, an invalid digit at each of the four positions, and 3,000 seeded random escapes.unit-locale-cpp.cppchecks a multi-byte decimal point fordoubleexactly and forlong doubleagainst the "C" locale.strtod_l/strtof_lover several million random tokens and the full 240,187 hard cases; clean under ASan/UBSan and the CI's compiler warning flags, including GCC 16, clang-tidy, and x86_64 under Rosetta (x87long double).Generator of
float_hard_cases.hppThe 508 embedded cases are produced by
compact_hard_cases.py 5(importinghard_cases.py, the fuller generator used for the 240,187-case offline checks), then formatted as one C++ initializer per output line. For every format boundary and a set of random values, it computes the exact midpoint to the next representable value and emits that midpoint, one unit above/below it, the midpoint with digits appended just past the rounding boundary, and the midpoint truncated at several digit counts around 17–30, in all three JSON number notations, with 30% negative. Both scripts round with exact rational arithmetic and cross-check against Python'sfloat().Public API
No breaking changes. Everything added or removed is in
nlohmann::detail. Parsed values are unchanged wherever the previous conversion was correctly rounded; they change only where it was not (a locale with a multi-byte decimal point, or a C library whosestrtodis not correctly rounded). No change todump()or any other output.Fixes #5660.
Written by Claude Code.
🤖 Generated with Claude Code