From d96697c28534feaf6a5f68490c48821f795c04f6 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 23 Oct 2026 07:59:31 +0200 Subject: [PATCH] Grow the decode arena through a function and store its position back on every return slow_string() kept its write position in locals and grew the arena through the macro NLOHMANN_VIEW_ENSURE. The macro is now the always-inline member arena_ensure() (a lambda is not inlined at every call site by clang, which then keeps the position in memory: +11 % instructions on escape-heavy strings), and a guard stores the position back into m_arena_write on every return instead of by hand on the successful one. Instruction counts are unchanged (clang) or 1 % lower (GCC). Also word the complexity of back() as that of at(). Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json_view/back.md | 2 +- include/nlohmann/detail/view/builder.hpp | 71 +++++++++++++++----- single_include/nlohmann/json_view.hpp | 71 +++++++++++++++----- 3 files changed, 109 insertions(+), 35 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_view/back.md b/docs/mkdocs/docs/api/basic_json_view/back.md index 424e2cfa0..acb6ebd65 100644 --- a/docs/mkdocs/docs/api/basic_json_view/back.md +++ b/docs/mkdocs/docs/api/basic_json_view/back.md @@ -28,7 +28,7 @@ This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics. Linear in the size of the array or object: unlike [`front()`](front.md), which only ever looks at the first element, `back()` has to walk every element to find where they end, since elements are not a fixed size in the -index. Constant for an array none of whose elements is an array or object, since the elements are then adjacent +index. Constant if no element of the array is an array or object, since the elements are then adjacent in the index. ## Notes diff --git a/include/nlohmann/detail/view/builder.hpp b/include/nlohmann/detail/view/builder.hpp index e73ca355a..3e4ea268a 100644 --- a/include/nlohmann/detail/view/builder.hpp +++ b/include/nlohmann/detail/view/builder.hpp @@ -277,26 +277,25 @@ class builder }; // The write position is kept in locals: the stores of characters into // the arena may alias the members of the builder, which would then be - // reloaded after every one. It is stored back where the arena grows - // and at the end. + // reloaded after every one. It is stored back where the arena grows, + // and by `write_back` on every return. const unsigned char* const end = m_end; const std::size_t start = arena_used(); char* w = m_arena_write; const char* w_end = m_arena_end; -#define NLOHMANN_VIEW_ENSURE(n) \ - if (NLOHMANN_VIEW_UNLIKELY(static_cast(w_end - w) < (n))) { m_arena_write = w; arena_grow(n); w = m_arena_write; w_end = m_arena_end; } + const arena_write_back write_back(*this, w); { const auto n = static_cast(p - s); if (n <= 16 && end - s >= 16) { - NLOHMANN_VIEW_ENSURE(16) + arena_ensure(w, w_end, 16); std::memcpy(w, s, 16); } else if (n != 0) { // (an empty run leaves an empty arena unallocated, and memcpy() // must not be passed its null pointer even for zero bytes) - NLOHMANN_VIEW_ENSURE(n) + arena_ensure(w, w_end, n); std::memcpy(w, s, n); } w += n; @@ -311,8 +310,9 @@ class builder if (c == '"') { ++p; - m_arena_write = w; - return decoded{p, start, arena_used() - start}; + // (from w: `write_back` stores it only after the return value + // is made; the arena is allocated, since an escape was written) + return decoded{p, start, static_cast(w - m_doc.arena.data()) - start}; } if (c != '\\') { @@ -356,12 +356,12 @@ class builder } cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } - NLOHMANN_VIEW_ENSURE(4) + arena_ensure(w, w_end, 4); w = put_utf8(w, cp); } else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0)) { - NLOHMANN_VIEW_ENSURE(1) + arena_ensure(w, w_end, 1); *w++ = simple_escape[d]; } else @@ -387,7 +387,7 @@ class builder NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) #undef NLOHMANN_VIEW_STEP run_found: - NLOHMANN_VIEW_ENSURE(16) + arena_ensure(w, w_end, 16); std::memcpy(w, p, 16); // (only the first `steps` bytes are kept) w += steps; p += steps; @@ -399,7 +399,7 @@ run_found: const unsigned char* const r = p; p = scan_string_run(p, end); const auto n = static_cast(p - r); - NLOHMANN_VIEW_ENSURE(n) + arena_ensure(w, w_end, n); std::memcpy(w, r, n); w += n; } @@ -423,12 +423,12 @@ run_found: const unsigned char* const r = p; p = scan_string_run(p, end); const auto n = static_cast(p - r); - NLOHMANN_VIEW_ENSURE(n) + arena_ensure(w, w_end, n); std::memcpy(w, r, n); w += n; break; } - NLOHMANN_VIEW_ENSURE(16) + arena_ensure(w, w_end, 16); const unsigned k = copy_plain_run16(p, w); w += k; p += k; @@ -442,7 +442,7 @@ run_found: const unsigned char* const r = p; p = scan_string_run(p, end); const auto n = static_cast(p - r); - NLOHMANN_VIEW_ENSURE(n) + arena_ensure(w, w_end, n); std::memcpy(w, r, n); w += n; } @@ -452,12 +452,11 @@ run_found: const unsigned char* const r = p; p = scan_string_run(p, end); const auto n = static_cast(p - r); - NLOHMANN_VIEW_ENSURE(n) + arena_ensure(w, w_end, n); std::memcpy(w, r, n); w += n; #endif } -#undef NLOHMANN_VIEW_ENSURE } /// does the float token [s, p) overflow FloatType? (parse() rejects it) @@ -498,6 +497,44 @@ run_found: char* m_arena_write = nullptr; char* m_arena_end = nullptr; + /// room for n more bytes at w, the write position kept in a local, with + /// w_end the end of the arena. (A lambda instead is not always inlined, + /// and then w would be kept in memory, as m_arena_write is.) + NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(char*& w, const char*& w_end, std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(w_end - w) < n)) + { + m_arena_write = w; + arena_grow(n); + w = m_arena_write; + w_end = m_arena_end; + } + } + + /// stores a write position kept in a local back into m_arena_write when + /// the function that keeps it returns, on every path + class arena_write_back + { + public: + arena_write_back(builder& b, char*& w) noexcept + : m_builder(b) + , m_w(w) + {} + ~arena_write_back() + { + m_builder.m_arena_write = m_w; + } + + arena_write_back(const arena_write_back&) = delete; + arena_write_back& operator=(const arena_write_back&) = delete; + arena_write_back(arena_write_back&&) = delete; + arena_write_back& operator=(arena_write_back&&) = delete; + + private: + builder& m_builder; + char*& m_w; + }; + NLOHMANN_VIEW_ALWAYS_INLINE std::size_t arena_used() const noexcept { return m_arena_write != nullptr ? static_cast(m_arena_write - m_doc.arena.data()) : 0; diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 7a3ad2a14..bfc0fc496 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -1468,26 +1468,25 @@ class builder }; // The write position is kept in locals: the stores of characters into // the arena may alias the members of the builder, which would then be - // reloaded after every one. It is stored back where the arena grows - // and at the end. + // reloaded after every one. It is stored back where the arena grows, + // and by `write_back` on every return. const unsigned char* const end = m_end; const std::size_t start = arena_used(); char* w = m_arena_write; const char* w_end = m_arena_end; -#define NLOHMANN_VIEW_ENSURE(n) \ - if (NLOHMANN_VIEW_UNLIKELY(static_cast(w_end - w) < (n))) { m_arena_write = w; arena_grow(n); w = m_arena_write; w_end = m_arena_end; } + const arena_write_back write_back(*this, w); { const auto n = static_cast(p - s); if (n <= 16 && end - s >= 16) { - NLOHMANN_VIEW_ENSURE(16) + arena_ensure(w, w_end, 16); std::memcpy(w, s, 16); } else if (n != 0) { // (an empty run leaves an empty arena unallocated, and memcpy() // must not be passed its null pointer even for zero bytes) - NLOHMANN_VIEW_ENSURE(n) + arena_ensure(w, w_end, n); std::memcpy(w, s, n); } w += n; @@ -1502,8 +1501,9 @@ class builder if (c == '"') { ++p; - m_arena_write = w; - return decoded{p, start, arena_used() - start}; + // (from w: `write_back` stores it only after the return value + // is made; the arena is allocated, since an escape was written) + return decoded{p, start, static_cast(w - m_doc.arena.data()) - start}; } if (c != '\\') { @@ -1547,12 +1547,12 @@ class builder } cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } - NLOHMANN_VIEW_ENSURE(4) + arena_ensure(w, w_end, 4); w = put_utf8(w, cp); } else if (NLOHMANN_VIEW_LIKELY(d < 128 && simple_escape[d] != 0)) { - NLOHMANN_VIEW_ENSURE(1) + arena_ensure(w, w_end, 1); *w++ = simple_escape[d]; } else @@ -1578,7 +1578,7 @@ class builder NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) #undef NLOHMANN_VIEW_STEP run_found: - NLOHMANN_VIEW_ENSURE(16) + arena_ensure(w, w_end, 16); std::memcpy(w, p, 16); // (only the first `steps` bytes are kept) w += steps; p += steps; @@ -1590,7 +1590,7 @@ run_found: const unsigned char* const r = p; p = scan_string_run(p, end); const auto n = static_cast(p - r); - NLOHMANN_VIEW_ENSURE(n) + arena_ensure(w, w_end, n); std::memcpy(w, r, n); w += n; } @@ -1614,12 +1614,12 @@ run_found: const unsigned char* const r = p; p = scan_string_run(p, end); const auto n = static_cast(p - r); - NLOHMANN_VIEW_ENSURE(n) + arena_ensure(w, w_end, n); std::memcpy(w, r, n); w += n; break; } - NLOHMANN_VIEW_ENSURE(16) + arena_ensure(w, w_end, 16); const unsigned k = copy_plain_run16(p, w); w += k; p += k; @@ -1633,7 +1633,7 @@ run_found: const unsigned char* const r = p; p = scan_string_run(p, end); const auto n = static_cast(p - r); - NLOHMANN_VIEW_ENSURE(n) + arena_ensure(w, w_end, n); std::memcpy(w, r, n); w += n; } @@ -1643,12 +1643,11 @@ run_found: const unsigned char* const r = p; p = scan_string_run(p, end); const auto n = static_cast(p - r); - NLOHMANN_VIEW_ENSURE(n) + arena_ensure(w, w_end, n); std::memcpy(w, r, n); w += n; #endif } -#undef NLOHMANN_VIEW_ENSURE } /// does the float token [s, p) overflow FloatType? (parse() rejects it) @@ -1689,6 +1688,44 @@ run_found: char* m_arena_write = nullptr; char* m_arena_end = nullptr; + /// room for n more bytes at w, the write position kept in a local, with + /// w_end the end of the arena. (A lambda instead is not always inlined, + /// and then w would be kept in memory, as m_arena_write is.) + NLOHMANN_VIEW_ALWAYS_INLINE void arena_ensure(char*& w, const char*& w_end, std::size_t n) + { + if (NLOHMANN_VIEW_UNLIKELY(static_cast(w_end - w) < n)) + { + m_arena_write = w; + arena_grow(n); + w = m_arena_write; + w_end = m_arena_end; + } + } + + /// stores a write position kept in a local back into m_arena_write when + /// the function that keeps it returns, on every path + class arena_write_back + { + public: + arena_write_back(builder& b, char*& w) noexcept + : m_builder(b) + , m_w(w) + {} + ~arena_write_back() + { + m_builder.m_arena_write = m_w; + } + + arena_write_back(const arena_write_back&) = delete; + arena_write_back& operator=(const arena_write_back&) = delete; + arena_write_back(arena_write_back&&) = delete; + arena_write_back& operator=(arena_write_back&&) = delete; + + private: + builder& m_builder; + char*& m_w; + }; + NLOHMANN_VIEW_ALWAYS_INLINE std::size_t arena_used() const noexcept { return m_arena_write != nullptr ? static_cast(m_arena_write - m_doc.arena.data()) : 0;