Compare commits

..
Author SHA1 Message Date
Claude d479d34677 Un-nest the run-length ternary in the string escaper
clang-tidy's readability-avoid-nested-conditional-operator (newly enforced
by the ci_clang_tidy job's clang) rejected the nested ?: that picked the
run length: EnsureAscii ? (is_ascii_copyable(...) ? scan : 0) : bulk.

Compute it in an immediately-invoked lambda instead. The value stays const
and the laziness is unchanged - the ascii scan still runs only on the
ensure_ascii path when the first byte is copyable, and string_bulk_run only
on the other path (EnsureAscii is a template bool, so the dead branch is
folded away). Output is byte-for-byte identical; single_include regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1oJ2ggRHS37zeVe94QTA1
Signed-off-by: Claude <noreply@anthropic.com>
2026-09-02 22:42:18 +00:00
Niels LohmannandClaude 09ae666612 Take the output adapter by reference at the serializer ctor
Per review: the serializer still holds the adapter as a non-owning
pointer, but the constructor now takes output_adapter_protocol<char>&
and takes its address internally, so every call site passes a
reference. A reference cannot be null and reads as a borrow, which
makes the lifetime contract harder to get wrong than handing over a
raw pointer. The stored member and the write path are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAYM1qhSA2FDaDcGfPW3fG
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-02 22:29:20 +02:00
Claude 78f71358c1 Update the serializer test for the non-owning adapter ctor
check_escaped constructed the serializer with output_adapter<char>(ss),
which produced the old owning output_adapter_t. The ctor now takes a
non-owning output_adapter_protocol<char>*, so build the concrete
output_stream_adapter on the stack and pass its address, matching how
dump() and operator<< now call it.

Signed-off-by: Claude <noreply@anthropic.com>
2026-09-02 22:29:19 +02:00
Claude f707a2f997 Stop dump() from heap-allocating its output adapter per call
The serializer held its output sink as output_adapter_t<char>
(a std::shared_ptr<output_adapter_protocol<char>>), which dump() and
operator<< built via make_shared -- one heap allocation per call for a
sink that only wraps a reference to the caller's string or stream.

Hold the sink as a non-owning output_adapter_protocol<char>* instead and
construct the concrete adapter on the stack at the call site. The write
path (o->write_characters) is unchanged, so output is byte-for-byte
identical; a compact dump() of a small object drops from 2 heap
allocations to 1 (only the returned string remains), ~3% faster.

Completes the per-call allocation cleanup on this branch, which already
removed the indent_string buffer (both were reported in #5413).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1oJ2ggRHS37zeVe94QTA1
Signed-off-by: Claude <noreply@anthropic.com>
2026-09-02 22:29:19 +02:00
Niels Lohmann 3426a41391 Do not scan for a copyable run that cannot exist
Under ensure_ascii, dump_escaped() calls find_ascii_copyable_run() at every
character boundary. When the text is dense non-ASCII - CJK, where every byte
is >= 0x80 - the scanner stops on its first byte and returns zero, so its SWAR
block runs once per character and buys nothing, on top of the escaping that
still has to happen afterwards.

A run can only be non-empty when the first byte is one the scanner may copy,
so test that single byte before calling it. Runs that do exist are found
exactly as before, so the bulk-copy win is unchanged; only the calls that were
always going to return zero are skipped.

Output is unchanged: the dump digest over canada/citm/twitter, in compact,
pretty and ensure_ascii form, matches develop byte for byte.

  dump(ensure_ascii=true)   develop    before     after
  CJK text                   3.54ms    4.25ms    3.36ms
  CJK, no ASCII at all       3.09ms    4.02ms    3.02ms
  Latin-1-ish text           4.39ms    3.04ms    2.93ms
  plain ASCII                3.92ms    0.80ms    0.79ms

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-02 22:28:50 +02:00
3 changed files with 18 additions and 54 deletions
@@ -850,15 +850,16 @@ class serializer
// for the scan. Without it, text whose characters all have to be // for the scan. Without it, text whose characters all have to be
// escaped - CJK under ensure_ascii, where every byte is >= 0x80 - // escaped - CJK under ensure_ascii, where every byte is >= 0x80 -
// runs the scanner once per character only to be told zero. // runs the scanner once per character only to be told zero.
std::size_t run = 0; const std::size_t run = [&]() -> std::size_t
if (!EnsureAscii)
{ {
run = string_bulk_run(data + i, s.size() - i); if (EnsureAscii)
}
else if (is_ascii_copyable(data[i]))
{ {
run = find_ascii_copyable_run(data + i, s.size() - i); return is_ascii_copyable(data[i])
? find_ascii_copyable_run(data + i, s.size() - i)
: 0;
} }
return string_bulk_run(data + i, s.size() - i);
}();
if (run != 0) if (run != 0)
{ {
// emit any bytes still pending in string_buffer first to // emit any bytes still pending in string_buffer first to
+7 -6
View File
@@ -21910,15 +21910,16 @@ class serializer
// for the scan. Without it, text whose characters all have to be // for the scan. Without it, text whose characters all have to be
// escaped - CJK under ensure_ascii, where every byte is >= 0x80 - // escaped - CJK under ensure_ascii, where every byte is >= 0x80 -
// runs the scanner once per character only to be told zero. // runs the scanner once per character only to be told zero.
std::size_t run = 0; const std::size_t run = [&]() -> std::size_t
if (!EnsureAscii)
{ {
run = string_bulk_run(data + i, s.size() - i); if (EnsureAscii)
}
else if (is_ascii_copyable(data[i]))
{ {
run = find_ascii_copyable_run(data + i, s.size() - i); return is_ascii_copyable(data[i])
? find_ascii_copyable_run(data + i, s.size() - i)
: 0;
} }
return string_bulk_run(data + i, s.size() - i);
}();
if (run != 0) if (run != 0)
{ {
// emit any bytes still pending in string_buffer first to // emit any bytes still pending in string_buffer first to
-38
View File
@@ -81,44 +81,6 @@ BENCHMARK_CAPTURE(ParseString, signed_ints, TEST_DATA_DIRECTORY "/regressi
BENCHMARK_CAPTURE(ParseString, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json"); BENCHMARK_CAPTURE(ParseString, unsigned_ints, TEST_DATA_DIRECTORY "/regression/unsigned_ints.json");
BENCHMARK_CAPTURE(ParseString, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json"); BENCHMARK_CAPTURE(ParseString, small_signed_ints, TEST_DATA_DIRECTORY "/regression/small_signed_ints.json");
//////////////////////////////////////////////////////////////////////////////
// parse pretty-printed JSON from string
//
// Every file in the corpus above is minified or only lightly spaced, so none of
// them exercise the lexer's whitespace handling. Real-world JSON is frequently
// indented - configuration files, pretty-printed API responses, anything kept
// under version control - where insignificant whitespace can outweigh the data.
// Re-serializing a document with an indentation and parsing that keeps the
// content identical to the ParseString row above, so the pair isolates the cost
// of the whitespace alone.
//////////////////////////////////////////////////////////////////////////////
static void ParseIndented(benchmark::State& state, const char* filename, int indent)
{
std::ifstream f(filename);
std::string str((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
const std::string indented = json::parse(str).dump(indent);
while (state.KeepRunning())
{
state.PauseTiming();
auto* j = new json();
state.ResumeTiming();
*j = json::parse(indented);
state.PauseTiming();
delete j;
state.ResumeTiming();
}
state.SetBytesProcessed(state.iterations() * indented.size());
}
BENCHMARK_CAPTURE(ParseIndented, jeopardy / 4, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json", 4);
BENCHMARK_CAPTURE(ParseIndented, canada / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json", 4);
BENCHMARK_CAPTURE(ParseIndented, citm_catalog / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", 4);
BENCHMARK_CAPTURE(ParseIndented, twitter / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", 4);
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
// serialize JSON // serialize JSON
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////