From 8bbfb5ec1d5483c2934c1371da57c59277ec89c5 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 31 Oct 2026 17:51:51 +0100 Subject: [PATCH 1/2] CBOR reader: enter the scalar run only when a scalar follows An array of containers or strings called the out-of-line run start once per array only to leave it at once; on nested data (arrays of objects) that call made reading slower than before the run reader. enter_cbor_array now tests the first byte and the remaining length inline and calls the run only for a scalar, so the run's loop condition moves to the end (do-while). Signed-off-by: Niels Lohmann --- .../nlohmann/detail/input/binary_reader.hpp | 28 +++++++++++++++++-- single_include/nlohmann/json.hpp | 28 +++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 5c7f9e10e..4bcd98022 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -1702,12 +1702,15 @@ class binary_reader than 9 bytes (the longest of these values) are left in the input, leaving those to @ref parse_cbor_value, which reads them the same way. + The caller has checked that an element remains, that at least 9 bytes are + left, and that the next byte begins a scalar. + @param[in,out] remaining the number of elements of the array not yet read @return whether the SAX parser accepted the values */ bool parse_cbor_scalar_run(std::size_t& remaining) { - while (remaining != 0 && ia.bulk_remaining() >= 9) + do { const auto* const data = reinterpret_cast(ia.bulk_data()); const unsigned char head = data[0]; @@ -1795,6 +1798,7 @@ class binary_reader return false; } } + while (remaining != 0 && ia.bulk_remaining() >= 9); return true; } @@ -1811,7 +1815,27 @@ class binary_reader */ bool enter_cbor_array(const std::size_t len) { - return enter_array(len) && (len == 0 || parse_cbor_array_start(std::integral_constant {})); + return enter_array(len) && (len == 0 || !cbor_scalar_follows(std::integral_constant {}) + || parse_cbor_array_start(std::integral_constant {})); + } + + /// whether a run of scalars can start at the next byte: at least 9 bytes + /// are left and the byte begins an integer, a float, a boolean, or null; + /// one test, so that an array of containers or strings makes no call + bool cbor_scalar_follows(std::true_type /*bulk*/) const + { + if (ia.bulk_remaining() < 9) + { + return false; + } + const auto head = static_cast(*ia.bulk_data()); + return head < 0x40 || head >= 0xF4; + } + + /// input that is not contiguous: every value is read by parse_cbor_value + bool cbor_scalar_follows(std::false_type /*bulk*/) const noexcept + { + return false; } /// read the scalars the array just opened begins with; out of line, so diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index b018dcdc6..22022146a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19478,12 +19478,15 @@ class binary_reader than 9 bytes (the longest of these values) are left in the input, leaving those to @ref parse_cbor_value, which reads them the same way. + The caller has checked that an element remains, that at least 9 bytes are + left, and that the next byte begins a scalar. + @param[in,out] remaining the number of elements of the array not yet read @return whether the SAX parser accepted the values */ bool parse_cbor_scalar_run(std::size_t& remaining) { - while (remaining != 0 && ia.bulk_remaining() >= 9) + do { const auto* const data = reinterpret_cast(ia.bulk_data()); const unsigned char head = data[0]; @@ -19571,6 +19574,7 @@ class binary_reader return false; } } + while (remaining != 0 && ia.bulk_remaining() >= 9); return true; } @@ -19587,7 +19591,27 @@ class binary_reader */ bool enter_cbor_array(const std::size_t len) { - return enter_array(len) && (len == 0 || parse_cbor_array_start(std::integral_constant {})); + return enter_array(len) && (len == 0 || !cbor_scalar_follows(std::integral_constant {}) + || parse_cbor_array_start(std::integral_constant {})); + } + + /// whether a run of scalars can start at the next byte: at least 9 bytes + /// are left and the byte begins an integer, a float, a boolean, or null; + /// one test, so that an array of containers or strings makes no call + bool cbor_scalar_follows(std::true_type /*bulk*/) const + { + if (ia.bulk_remaining() < 9) + { + return false; + } + const auto head = static_cast(*ia.bulk_data()); + return head < 0x40 || head >= 0xF4; + } + + /// input that is not contiguous: every value is read by parse_cbor_value + bool cbor_scalar_follows(std::false_type /*bulk*/) const noexcept + { + return false; } /// read the scalars the array just opened begins with; out of line, so From 70a4b25cd2c54e3f5a7dfd20d16d56cf612371a2 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 31 Oct 2026 17:52:47 +0100 Subject: [PATCH 2/2] MessagePack reader: enter the scalar run only when a scalar follows As for CBOR: enter_msgpack_array tests the first byte and the remaining length inline and calls the out-of-line run only for a scalar, so arrays of maps or strings no longer pay for a call that reads nothing; the run's loop condition moves to the end (do-while). Signed-off-by: Niels Lohmann --- .../nlohmann/detail/input/binary_reader.hpp | 29 +++++++++++++++++-- single_include/nlohmann/json.hpp | 29 +++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 72498198e..28af01bee 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -2642,12 +2642,15 @@ class binary_reader than 9 bytes (the longest of these values) are left in the input, leaving those to @ref parse_msgpack_value, which reads them the same way. + The caller has checked that an element remains, that at least 9 bytes are + left, and that the next byte begins a scalar. + @param[in,out] remaining the number of elements of the array not yet read @return whether the SAX parser accepted the values */ bool parse_msgpack_scalar_run(std::size_t& remaining) { - while (remaining != 0 && ia.bulk_remaining() >= 9) + do { const auto* const data = reinterpret_cast(ia.bulk_data()); const unsigned char head = data[0]; @@ -2737,6 +2740,7 @@ class binary_reader return false; } } + while (remaining != 0 && ia.bulk_remaining() >= 9); return true; } @@ -2748,7 +2752,28 @@ class binary_reader */ bool enter_msgpack_array(const std::size_t len) { - return enter_array(len) && (len == 0 || parse_msgpack_array_start(std::integral_constant {})); + return enter_array(len) && (len == 0 || !msgpack_scalar_follows(std::integral_constant {}) + || parse_msgpack_array_start(std::integral_constant {})); + } + + /// whether a run of scalars can start at the next byte: at least 9 bytes + /// are left and the byte begins a fixint, nil, a boolean, a float, or a + /// sized integer (0xC0-0xD3); one test, so that an array of containers or + /// strings makes no call + bool msgpack_scalar_follows(std::true_type /*bulk*/) const + { + if (ia.bulk_remaining() < 9) + { + return false; + } + const auto head = static_cast(*ia.bulk_data()); + return head < 0x80 || head >= 0xE0 || (head >= 0xC0 && head <= 0xD3); + } + + /// input that is not contiguous: every value is read by parse_msgpack_value + bool msgpack_scalar_follows(std::false_type /*bulk*/) const noexcept + { + return false; } /// read the scalars the array just opened begins with; out of line, so diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 9682fc3da..9584e90a8 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -20419,12 +20419,15 @@ class binary_reader than 9 bytes (the longest of these values) are left in the input, leaving those to @ref parse_msgpack_value, which reads them the same way. + The caller has checked that an element remains, that at least 9 bytes are + left, and that the next byte begins a scalar. + @param[in,out] remaining the number of elements of the array not yet read @return whether the SAX parser accepted the values */ bool parse_msgpack_scalar_run(std::size_t& remaining) { - while (remaining != 0 && ia.bulk_remaining() >= 9) + do { const auto* const data = reinterpret_cast(ia.bulk_data()); const unsigned char head = data[0]; @@ -20514,6 +20517,7 @@ class binary_reader return false; } } + while (remaining != 0 && ia.bulk_remaining() >= 9); return true; } @@ -20525,7 +20529,28 @@ class binary_reader */ bool enter_msgpack_array(const std::size_t len) { - return enter_array(len) && (len == 0 || parse_msgpack_array_start(std::integral_constant {})); + return enter_array(len) && (len == 0 || !msgpack_scalar_follows(std::integral_constant {}) + || parse_msgpack_array_start(std::integral_constant {})); + } + + /// whether a run of scalars can start at the next byte: at least 9 bytes + /// are left and the byte begins a fixint, nil, a boolean, a float, or a + /// sized integer (0xC0-0xD3); one test, so that an array of containers or + /// strings makes no call + bool msgpack_scalar_follows(std::true_type /*bulk*/) const + { + if (ia.bulk_remaining() < 9) + { + return false; + } + const auto head = static_cast(*ia.bulk_data()); + return head < 0x80 || head >= 0xE0 || (head >= 0xC0 && head <= 0xD3); + } + + /// input that is not contiguous: every value is read by parse_msgpack_value + bool msgpack_scalar_follows(std::false_type /*bulk*/) const noexcept + { + return false; } /// read the scalars the array just opened begins with; out of line, so