From b6750e9911d7d28ee9de12e03db954904f3bc18d Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 5 Sep 2026 21:40:11 +0200 Subject: [PATCH] Fix codegen regression in skip_whitespace() from #5490 Benchmarking found the get()/get_ignoring_pending_unget() split in skip_whitespace() made long whitespace runs (e.g. indentation in pretty-printed JSON) 1.75x-3.2x SLOWER instead of faster, reproducible with both Apple Clang and GCC. Root cause: rewriting the loop from a plain do-while into an initial get() followed by a while-loop defeated the compiler's ability to keep the input adapter's read/end pointers in registers across iterations; both compilers instead reloaded them from memory on every character. The function split itself was not the problem (it still fully inlines); the loop's control-flow shape was. The fix keeps the same two-function structure but restores a do-while shape (guarded by an if for the "first char not whitespace" case), which lets both compilers hoist the pointers back into registers, matching or beating pre-#5490 performance. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/input/lexer.hpp | 15 +++++++++++++-- single_include/nlohmann/json.hpp | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/include/nlohmann/detail/input/lexer.hpp b/include/nlohmann/detail/input/lexer.hpp index b5cc8d712..5861bd49d 100644 --- a/include/nlohmann/detail/input/lexer.hpp +++ b/include/nlohmann/detail/input/lexer.hpp @@ -1705,9 +1705,20 @@ scan_number_done: // nothing below calls unget() get(); - while (current == ' ' || current == '\t' || current == '\n' || current == '\r') + // this is written as an if-guarded do-while (rather than a plain + // while loop) because that shape is what lets both GCC and Clang + // keep the input adapter's read pointer in a register across + // iterations; the equivalent while-loop measurably defeated that + // optimization in testing, turning long whitespace runs (e.g. the + // indentation of pretty-printed JSON) from a register-only loop + // into one that reloads the pointer from memory every character + if (current == ' ' || current == '\t' || current == '\n' || current == '\r') { - get_ignoring_pending_unget(); + do + { + get_ignoring_pending_unget(); + } + while (current == ' ' || current == '\t' || current == '\n' || current == '\r'); } } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 5532ce752..2a787ce8a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -9488,9 +9488,20 @@ scan_number_done: // nothing below calls unget() get(); - while (current == ' ' || current == '\t' || current == '\n' || current == '\r') + // this is written as an if-guarded do-while (rather than a plain + // while loop) because that shape is what lets both GCC and Clang + // keep the input adapter's read pointer in a register across + // iterations; the equivalent while-loop measurably defeated that + // optimization in testing, turning long whitespace runs (e.g. the + // indentation of pretty-printed JSON) from a register-only loop + // into one that reloads the pointer from memory every character + if (current == ' ' || current == '\t' || current == '\n' || current == '\r') { - get_ignoring_pending_unget(); + do + { + get_ignoring_pending_unget(); + } + while (current == ' ' || current == '\t' || current == '\n' || current == '\r'); } }