From 3426a41391bceb1373d5267e2d3aaa30d239fbd5 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 2 Sep 2026 22:28:50 +0200 Subject: [PATCH] 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 --- include/nlohmann/detail/output/serializer.hpp | 9 ++++++++- single_include/nlohmann/json.hpp | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 20a65d76e..80ee84c8c 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -844,8 +844,15 @@ class serializer if (state == UTF8_ACCEPT) { const auto* const data = reinterpret_cast(s.data()); + // A run can only be non-empty when the very first byte is one + // the scanner may copy, so test that single byte before paying + // for the scan. Without it, text whose characters all have to be + // escaped - CJK under ensure_ascii, where every byte is >= 0x80 - + // runs the scanner once per character only to be told zero. const std::size_t run = EnsureAscii - ? find_ascii_copyable_run(data + i, s.size() - i) + ? (is_ascii_copyable(data[i]) + ? find_ascii_copyable_run(data + i, s.size() - i) + : 0) : string_bulk_run(data + i, s.size() - i); if (run != 0) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 04438d1f7..26d0943da 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -21904,8 +21904,15 @@ class serializer if (state == UTF8_ACCEPT) { const auto* const data = reinterpret_cast(s.data()); + // A run can only be non-empty when the very first byte is one + // the scanner may copy, so test that single byte before paying + // for the scan. Without it, text whose characters all have to be + // escaped - CJK under ensure_ascii, where every byte is >= 0x80 - + // runs the scanner once per character only to be told zero. const std::size_t run = EnsureAscii - ? find_ascii_copyable_run(data + i, s.size() - i) + ? (is_ascii_copyable(data[i]) + ? find_ascii_copyable_run(data + i, s.size() - i) + : 0) : string_bulk_run(data + i, s.size() - i); if (run != 0) {