Skip to content

Construct remaining hot-path exceptions out of line so core functions inline - #5838

Merged
nlohmann merged 2 commits into
perf/inline-value-lifecyclefrom
perf/out-of-line-throws
Oct 15, 2026
Merged

nlohmann merged 2 commits into
perf/inline-value-lifecyclefrom
perf/out-of-line-throws

Conversation

@nlohmann

@nlohmann nlohmann commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Part of the inlining-blocker sweep (Clang -Rpass-missed=inline + assembly inspection): inline exception construction (concat(...), create(...)) keeps get_ref, operator[](size_type), the iterator operators and erase/insert out of line.

Changes

  • New detail::throw_literal<Exc>(id, const char*, ctx) (NO_RETURN + NEVER_INLINE) used by the 19 literal throw sites in json.hpp (json_value ctor, constructors, erase ×4, insert ×10, update) and 8 in iter_impl (key(), operator< ×2, +=, -, [], ==, throw_cannot_get_value).
  • NEVER_INLINE helpers for the message-building sites: get_ref_impl, non-const operator[](size_type) length error, iterator-range constructor.
  • All out-of-line throw work lives here, including at() (taken over from Speed up array-to-container from_json #5833): at(size_type) (const and non-const), erase(size_type) share one throw_array_index_out_of_range(idx, j) helper; the key-not-found throw of at(key) (json and ordered_json) uses throw_key_not_found(key, j).
  • iter_impl::throw_cannot_get_value() (from Use and extend the detail helpers to remove duplicated code #5783) is itself marked NEVER_INLINE instead of wrapping it.
  • NEVER_INLINE added to throw_type_must_be / throw_cannot_use_with; ordered_map::at_impl throws via a helper.
  • New unit-throw_messages.cpp pins id and what() text of the moved sites (including at(idx) const/non-const, at(key) for json/ordered_json, erase(idx)) (passes identically on the old and new headers). Exception types, ids and messages are unchanged.

Numbers (Apple clang 21, -O2)

before after
get_ref<const string&> 1.16 ns 0.64 ns
non-const arr[i] 1.60 ns 0.97 ns
iterator += - [] < 6.0 ns 0.95 ns
erase(it) + push_back 7.4 ns 5.6 ns
insert + erase 9.7 ns 8.7 ns
std::sort of 10k ints −6–9 %
at(i) in-range, const + non-const mix 2.02 ns 1.46 ns

GCC 16: erase/insert −20–35 %, caller frames shrink (get_ref 192→16 B), otherwise neutral. Parsing unchanged. Code size −2–3 % at -O2; -Os is within noise (+0.5 % on the bench binary). JSON_NOEXCEPTION and JSON_THROW_USER builds compile.

Coordination

This PR now owns every out-of-line throw, so #5833 shrinks to from_json. Expect textual conflicts with #5835 (iter_impl array fast path, throw_compare_different_values); I will rebase after it merges.

Breaking changes

None. No public API changes; exception types, ids and messages are byte-identical to develop. Only private/detail helpers were added.

🤖 Generated with Claude Code

Comment thread include/nlohmann/detail/exceptions.hpp Outdated
*/
template<typename BasicJsonType>
JSON_HEDLEY_NO_RETURN inline void throw_type_must_be(const char* expected, const BasicJsonType& j)
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE inline void throw_type_must_be(const char* expected, const BasicJsonType& j)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need inline on function templates. (Full specializations are not templates, and so do need it.) That eliminates the initial confusion of ...NEVER_INLINE followed by inline.

@nlohmann nlohmann added this to the Release 3.13.0 milestone Oct 15, 2026
nlohmann added a commit that referenced this pull request Oct 15, 2026
The at() cold-throw work moves to #5838; this PR keeps only the
array-to-container from_json speed-up and its benchmark.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann

Copy link
Copy Markdown
Owner Author

Written by Claude Code on behalf of @nlohmann.

Changes since the approval, following the decisions on the #5783 overlap (head 971330a):

  • This PR now owns all out-of-line throw work. at(size_type) (const and non-const) and the key-not-found throw of at(key) moved here from Speed up array-to-container from_json #5833 (which shrinks to from_json).
  • One helper per message: throw_array_index_out_of_range(idx, j) serves at() and erase(idx) (replacing throw_array_index_out_of_range_erase); throw_key_not_found(key, j) serves at(key).
  • iter_impl::throw_cannot_get_value() (Use and extend the detail helpers to remove duplicated code #5783) is now itself NEVER_INLINE with its original JSON_THROW body, rather than forwarding to throw_literal.
  • unit-throw_messages additionally covers at(idx) const/non-const, at(key) for json and ordered_json, and erase(idx). Messages and ids are unchanged; JSON_DIAGNOSTICS context unchanged.
  • Measured (Apple clang, -O2 -DNDEBUG, best of 15, alternating builds): in-range at(i) 2.02 → 1.46 ns/op.

@gregmarr could you take another look?

@nlohmann
nlohmann force-pushed the perf/out-of-line-throws branch from 971330a to 577067d Compare October 15, 2026 06:24
@nlohmann
nlohmann changed the base branch from develop to perf/inline-value-lifecycle October 15, 2026 06:24
@nlohmann
nlohmann added this pull request to stack #5841 October 15, 2026 06:24
@nlohmann nlohmann added the 🚀 ready to merge Ready to merge - just waiting for CI to complete. label Oct 15, 2026
@nlohmann
nlohmann force-pushed the perf/out-of-line-throws branch from 577067d to b0b7fcd Compare October 15, 2026 08:56
@nlohmann
nlohmann force-pushed the perf/out-of-line-throws branch from b0b7fcd to 0570c5b Compare October 15, 2026 09:54

/// @brief shared implementation of the const and non-const at() overloads
/// @throw std::out_of_range if @a key is not found
JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this before the comment block for at_impl, and optionally add its own comment block.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 433d4bb: throw_key_not_found() now sits before at_impl()'s comment block and has its own doc comment.

(Written by Claude Code.)

@nlohmann
nlohmann force-pushed the perf/out-of-line-throws branch from 433d4bb to 26bf1a0 Compare October 15, 2026 17:41
@nlohmann
nlohmann force-pushed the perf/out-of-line-throws branch from 26bf1a0 to c4cadd9 Compare October 15, 2026 17:43
@nlohmann
nlohmann force-pushed the perf/out-of-line-throws branch from c4cadd9 to 12a5527 Compare October 15, 2026 17:45
… inline

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann
nlohmann force-pushed the perf/out-of-line-throws branch from 12a5527 to 4569c02 Compare October 15, 2026 17:54
@nlohmann
nlohmann merged commit 6a27b9a into develop Oct 15, 2026
3 of 165 checks passed
@nlohmann
nlohmann deleted the perf/out-of-line-throws branch October 15, 2026 18:01
nlohmann added a commit that referenced this pull request Oct 15, 2026
These PRs were merged while their CI runs were cancelled, so the warnings
went unnoticed. All of them fail the -Werror builds:

- json_pointer.hpp (#5836): size_t += int under GCC -Wsign-conversion
- binary_reader.hpp (#5839): char_int_type passed as char under GCC
  -Wconversion
- json.hpp (#5838): @throw without a description under clang
  -Wdocumentation
- string_escape.hpp (#5836): escape_into() was inserted between escape()
  and its documentation, so the @return was attached to a void function
- unit-diagnostics.cpp (#5832): clang -Wnrvo in a lambda without an
  explicit return type
- unit-to_json-rvalue.cpp (#5837): GCC -Wnoexcept (C++20) and clang
  -Wunneeded-member-function on the test type

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Oct 16, 2026
- ci_test_noexceptions: the JSON Patch section of unit-throw_messages
  and the strict dumps in unit-serialization catch exceptions, which
  JSON_NOEXCEPTION turns into std::abort(); skip them there.
- ci_cppcheck: insert() and iter_impl::key() ended with a call to a
  noreturn throw helper, which Cppcheck reports as missingReturn; check
  the error case first and end with the return, like the other functions.
- ci_single_binaries: ordered_map.hpp uses the Hedley macros since #5838,
  so include hedley.hpp (IWYU).
- MSVC: also wrap the second vector<json> comparison in unit-constructor2
  in parentheses.

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