Skip to content

Construct the JSON Patch exceptions out of line - #5842

Merged
nlohmann merged 1 commit into
perf/binary-reader-cold-errorsfrom
perf/remaining-throw-sites
Oct 15, 2026
Merged

nlohmann merged 1 commit into
perf/binary-reader-cold-errorsfrom
perf/remaining-throw-sites

Conversation

@nlohmann

@nlohmann nlohmann commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

patch_inplace() built its error messages inline. In particular, get_value() concatenated the context-sensitive operation 'x' string on every call, even when the member was present. This PR moves the JSON Patch exceptions out of line:

  • Index and key errors reuse basic_json's existing throw_array_index_out_of_range and throw_key_not_found from Construct remaining hot-path exceptions out of line so core functions inline #5838.
  • The seven patch-specific messages each get one NEVER_INLINE helper in detail/exceptions.hpp: add/remove parent not a container, not an array, missing member, move into child, test failed, invalid operation.
  • get_value() now builds the operation 'x' string only inside the missing-member helper.

Exception ids and messages are byte-identical. unit-throw_messages gets a patch section that pins all 14 messages. It strips the (/0) prefix that JSON_DIAGNOSTICS adds, so the section passes in both configurations.

Measured

patch(diff(a, b)) on citm_catalog, best of 9, alternating builds:

Compiler Before (#5839) After Change
Apple clang, -O2 2.03–2.07 ms 1.89–1.96 ms −6.6 %
GCC 16, -O2 3.04–3.10 ms 2.75–2.79 ms −9.5 %

The GCC numbers come from the all-sites prototype, before it was reduced to the patch sites.

Deliberately left inline

The other exceptions that are still built inline were prototyped together and measured on parse, accept, dump, all binary writers and diff for citm, twitter and canada. Nothing moved beyond noise: ±2.8 % on clang, ±1.5 % on GCC. These sites stay as they are, because they are cold or already behind a call:

  • parser::syntax_error
  • 9 sites in binary_writer and throw_on_discarded
  • the two invalid UTF-8 sites in serializer
  • input_adapters' empty input
  • to_json's enum_key_duplicate
  • format_error (std::format parsing)

Performance summary of stack #5841

The table compares the top of the stack (this PR, which contains all eleven PRs) with develop (4e294f4).

  • Every row is the best of 3 alternating rounds, and each round already takes the best of 5–15 repetitions.
  • Compilers: Apple clang 21 (-O2 -DNDEBUG, arm64) and GCC 16 (-O2 -DNDEBUG, Docker on the same machine).
  • Documents: nativejson-benchmark's citm_catalog, twitter and canada.
PR Operation clang: develop → stack GCC 16: develop → stack
#5834 ordered_json::flatten() citm 257 → 2.4 ms (−99 %) 264 → 3.7 ms (−99 %)
#5834 json::flatten() citm 5.49 → 4.82 ms (−12 %) 7.82 → 6.88 ms (−12 %)
#5836 json_pointer(string) 199 → 101 ns (−49 %) 286 → 136 ns (−52 %)
#5836 json_pointer::to_string() 243 → 111 ns (−54 %) 380 → 82 ns (−78 %)
#5836 at(json_pointer) 77 → 58 ns (−25 %) 123 → 75 ns (−39 %)
#5836 contains(json_pointer) 102 → 54 ns (−47 %) 160 → 75 ns (−53 %)
#5836 unflatten() citm 21.8 → 13.3 ms (−39 %) 32.7 → 20.0 ms (−39 %)
#5833 get<std::vector<int>>, per element 4.45 → 0.95 ns (−79 %) 1.55 → 0.96 ns (−38 %)
#5833 canada get<vector<vector<vector<double>>>> 1.96 → 1.32 ms (−33 %) 1.53 → 1.47 ms (−4 %)
#5837 json(std::move(vector<string>)), 200k 14.3 → 9.2 ms (−36 %) 22.0 → 10.7 ms (−51 %)
#5832 JSON_DIAGNOSTICS: ordered_json 100k push_back 1564 → 1.5 ms (−99.9 %) 1643 → 1.1 ms (−99.9 %)
#5832 JSON_DIAGNOSTICS: ordered_json::update(), 20k keys 765 → 582 ms (−24 %) ¹ 773 → 544 ms (−30 %) ¹
#5832 JSON_DIAGNOSTICS: copy citm 1.73 → 1.56 ms (−10 %) 2.98 → 2.51 ms (−16 %)
#5840 copy a scalar json 2.88 → 0.95 ns (−67 %) 2.88 → 1.92 ns (−33 %)
#5840 j == 5 3.52 → 0.64 ns (−82 %) 2.88 → 1.91 ns (−34 %)
#5840 std::vector<json> of 1M ints, build + destroy 2.52 → 2.24 ms (−12 %) 1.06 → 0.93 ms (−12 %)
#5840 canada == canada 0.52 → 0.31 ms (−40 %) 0.52 → 0.50 ms (−4 %)
#5840 / #5835 copy canada 3.08 → 2.94 ms (−5 %) 4.25 → 3.78 ms (−11 %)
#5838 get_ref<const std::string&>() 1.16 → 0.32 ns (−73 %) 0.32 → 0.32 ns (±0)
#5838 at(i) 1.31 → 0.65 ns (−51 %) 0.70 → 0.54 ns (−22 %)
#5838 non-const arr[i] 1.59 → 0.87 ns (−45 %) 0.83 → 0.66 ns (−21 %)
#5838 / #5835 std::sort of 1M json ints 132 → 95 ms (−28 %) 122 → 82 ms (−33 %)
#5835 range-for over an array, per element 2.53 → 1.27 ns (−50 %) 1.12 → 0.96 ns (−14 %)
#5835 reverse iteration, per element 3.48 → 1.58 ns (−55 %) 1.12 → 0.96 ns (−14 %)
#5835 copy + destroy object of 100k ints 5.75 → 3.32 ms (−42 %) 5.18 → 3.01 ms (−42 %)
#5835 diff({}, citm) 3.24 → 1.50 ms (−54 %) 5.46 → 2.17 ms (−60 %)
#5835 copy + destroy citm 1.59 → 1.50 ms (−6 %) 2.58 → 2.19 ms (−15 %)
#5831 parse() citm with callback 4.21 → 3.97 ms (−6 %) 7.00 → 6.54 ms (−7 %)
#5839 from_cbor / from_msgpack / from_ubjson / from_bson citm −4 … −7 % −10 … −16 %
#5839 from_bjdata twitter 1.55 → 1.49 ms (−4 %) 2.73 → 2.60 ms (−5 %)
#5842 patch(diff(a, b)) citm −6.6 % (see above) −9.5 % (see above)

¹ The rest is ordered_map's linear key lookup, not the diagnostics. Without JSON_DIAGNOSTICS the same update() takes 269 ms on clang. The diagnostics overhead on top of it dropped from about 37 % to about 20 %.

Regression guard, parse/accept/dump on all three documents:

  • clang: within ±2 %, except parse of citm and canada, which is about 4 % faster.
  • GCC: within ±2 %, except parse of citm, which is 10 % faster.

Slower on GCC, small: citm == citm +3 %, diff(citm, citm) +4 %, ordered_json parse of citm with JSON_DIAGNOSTICS +3 %. The clang run-to-run spread on these rows is 2–8 %. #5840 describes the GCC == effect on large documents.

The benchmark source is standalone (one .cpp file, no Google Benchmark) and can be posted on request.

Breaking changes

No breaking changes. The new helpers are in nlohmann::detail, and exception types, ids and messages are unchanged.

🤖 Generated with Claude Code

@nlohmann
nlohmann added this pull request to stack #5841 October 15, 2026 06:33
@nlohmann nlohmann added performance review needed It would be great if someone could review the proposed changes. labels Oct 15, 2026
@nlohmann
nlohmann force-pushed the perf/remaining-throw-sites branch from f48141c to 513b7e4 Compare October 15, 2026 08:57
@nlohmann
nlohmann force-pushed the perf/remaining-throw-sites branch from 513b7e4 to 7bb0e41 Compare October 15, 2026 09:54
@nlohmann nlohmann added 🚀 ready to merge Ready to merge - just waiting for CI to complete. and removed review needed It would be great if someone could review the proposed changes. labels Oct 15, 2026
@nlohmann nlohmann added this to the Release 3.13.0 milestone Oct 15, 2026
@nlohmann
nlohmann force-pushed the perf/remaining-throw-sites branch from 7bb0e41 to e4b488a Compare October 15, 2026 17:41
@nlohmann
nlohmann force-pushed the perf/remaining-throw-sites branch from e4b488a to 75a9259 Compare October 15, 2026 17:43
@nlohmann
nlohmann force-pushed the perf/remaining-throw-sites branch from 75a9259 to 0711425 Compare October 15, 2026 17:45
patch_inplace() built its error messages inline. In particular get_value()
concatenated the context-sensitive "operation 'x'" string on every call, even
when nothing was wrong. The patch exceptions now come from NEVER_INLINE
helpers: the index and key errors reuse basic_json's existing
throw_array_index_out_of_range/throw_key_not_found, and the seven
patch-specific messages get one helper each in detail/exceptions.hpp. Ids
and messages are unchanged, and unit-throw_messages pins them (with and
without JSON_DIAGNOSTICS).

citm_catalog patch: -6.6 % (Apple clang), -9.5 % (GCC 16).
The other remaining throw sites (parser::syntax_error, binary_writer,
serializer, input_adapters, enum_key_duplicate, format_error) were prototyped
and measured: no gain beyond noise, so they stay as they are.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann
nlohmann force-pushed the perf/remaining-throw-sites branch from 0711425 to 8c1e549 Compare October 15, 2026 17:54
@nlohmann
nlohmann merged commit 6261d1e into develop Oct 15, 2026
3 of 165 checks passed
@nlohmann
nlohmann deleted the perf/remaining-throw-sites branch October 15, 2026 18:01
nlohmann added a commit that referenced this pull request Oct 16, 2026
Fixes every CI failure develop has had since the API performance stack (#5831-#5842) was merged: compiler warnings and errors (Clang, GCC 4.9/11, MSVC 2015/2017/2022), clang-tidy, cpplint, Cppcheck, IWYU, the examples check, the no-exceptions, diagnostic-positions, implicit-conversions and strict-NUL test configurations, and the MinGW link (now with LLD).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance 🚀 ready to merge Ready to merge - just waiting for CI to complete.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants