From 951271127bacede4e1b3bfc272013a7440ac5b94 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 7 Nov 2026 10:10:50 +0100 Subject: [PATCH] Binary readers: append strings straight from contiguous input (#6028) * Binary readers: append strings straight from contiguous input get_bytes() read every string into a zero-filled resize() and copied the bytes over it, so each byte was written twice. A string that the input holds in one block (bulk_remaining() at least its length) is now appended from it directly. The claimed length is checked against the input first, so a huge claimed length still allocates nothing it cannot fill; other inputs and binary values keep the chunked loop. from_cbor() and from_msgpack() of twitter.json and citm_catalog.json get 5.5-9.1% faster on an Apple M1 Max and 2.7-8.6% on x86-64 (GCC 15, Clang 21); canada.json, which has few strings, is unchanged. Signed-off-by: Niels Lohmann * Binary readers: keep the tag argument of append_from_block() astyle-proof Signed-off-by: Niels Lohmann * Fix CI: bugprone-signed-char-misuse in append_from_block Convert the (positive) length to its unsigned type before widening it to std::size_t, as clang-tidy flags the int8_t to std::size_t cast. Signed-off-by: Niels Lohmann * Tests: sweep_allocator::construct() accepts pointers to const Recent libc++ (emsdk latest) probes construct(const T*, const T&) in an unevaluated context while checking for trivial copies; the static_cast to void* in the noexcept specification then fails to compile. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01N5NasEsrzHhSNouxSoEwqz (cherry picked from commit 794bf70823c12a2a1cb2a8a95f34dfb6357746a8) Signed-off-by: Niels Lohmann * Tests: guard sweep_allocator::construct() against const types instead of const_cast The const_cast added in the previous commit is rejected by cppcoreguidelines-pro-type-const-cast (clang-tidy). Make construct() not viable for const U instead, which is what libc++ probes for. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01N5NasEsrzHhSNouxSoEwqz (cherry picked from commit de099fa607001b710862fd1bbecef3609c54cbc4) Signed-off-by: Niels Lohmann * Fix CI: astyle and clang-tidy for sweep_allocator::construct() astyle 3.4.13 reformats the enable_if default template argument (Check amalgamation). Delete the const overload instead, which makes the libc++ probe for const objects fail in the SFINAE context just the same and needs no . Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01N5NasEsrzHhSNouxSoEwqz Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann Co-authored-by: Claude Sonnet 5.5 --- .../nlohmann/detail/input/binary_reader.hpp | 38 +++++++++++++++++++ single_include/nlohmann/json.hpp | 38 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 93f707097..81a3ebd42 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -4887,6 +4887,14 @@ class binary_reader const char* context, ContainerType& result) { + // a string that is in the block of input as a whole is appended from + // it, without the zero-filling resize() below + constexpr bool from_block = bulk_scan && std::is_same::value; + if (append_from_block(len, result, std::integral_constant {})) + { + return true; + } + // upper bound on the number of bytes read (and allocated) per chunk constexpr std::size_t chunk_size = 4096; @@ -4923,6 +4931,36 @@ class binary_reader return true; } + /// append the @a len bytes at the current position to @a result, if the + /// input holds them in one block; false (with nothing read) otherwise + template + bool append_from_block(const NumberType len, string_t& result, std::true_type /*block*/) + { + if (len <= 0) + { + return false; + } + // (len > 0, so the unsigned type holds it; casting a signed char + // straight to std::size_t would trip bugprone-signed-char-misuse) + const auto ulen = static_cast::type>(len); + const std::size_t remaining = ia.bulk_remaining(); + if (static_cast(ulen) > static_cast(remaining)) + { + return false; // (the loop reports the premature end) + } + const auto n = static_cast(ulen); + result.append(reinterpret_cast(ia.bulk_data()), n); + ia.bulk_skip(n); + chars_read += n; + return true; + } + + template + static bool append_from_block(const NumberType /*len*/, ContainerType& /*result*/, std::false_type /*block*/) noexcept + { + return false; + } + /*! @param[in] context further context information (for diagnostics) @return whether the last read character is not EOF diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index eedb9c561..6e2ead1b4 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -22903,6 +22903,14 @@ class binary_reader const char* context, ContainerType& result) { + // a string that is in the block of input as a whole is appended from + // it, without the zero-filling resize() below + constexpr bool from_block = bulk_scan && std::is_same::value; + if (append_from_block(len, result, std::integral_constant {})) + { + return true; + } + // upper bound on the number of bytes read (and allocated) per chunk constexpr std::size_t chunk_size = 4096; @@ -22939,6 +22947,36 @@ class binary_reader return true; } + /// append the @a len bytes at the current position to @a result, if the + /// input holds them in one block; false (with nothing read) otherwise + template + bool append_from_block(const NumberType len, string_t& result, std::true_type /*block*/) + { + if (len <= 0) + { + return false; + } + // (len > 0, so the unsigned type holds it; casting a signed char + // straight to std::size_t would trip bugprone-signed-char-misuse) + const auto ulen = static_cast::type>(len); + const std::size_t remaining = ia.bulk_remaining(); + if (static_cast(ulen) > static_cast(remaining)) + { + return false; // (the loop reports the premature end) + } + const auto n = static_cast(ulen); + result.append(reinterpret_cast(ia.bulk_data()), n); + ia.bulk_skip(n); + chars_read += n; + return true; + } + + template + static bool append_from_block(const NumberType /*len*/, ContainerType& /*result*/, std::false_type /*block*/) noexcept + { + return false; + } + /*! @param[in] context further context information (for diagnostics) @return whether the last read character is not EOF