From a1381a619f440299726d42d49ece06e39c4ebc95 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 26 Sep 2026 07:43:16 +0200 Subject: [PATCH] Read BSON keys in bulk from contiguous input BSON keys (and array indices) are C-style strings, which were read byte by byte. For contiguous input they are now read up to their \x00-byte in one step, using the same bulk_scan flag as BON8 strings: twitter.json is read in 1.46 instead of 2.01 ms, citm_catalog.json in 2.93 instead of 3.33 ms, jeopardy.json in 182 instead of 207 ms. canada.json, whose keys are almost all one-digit array indices, takes 2 % longer. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/input/binary_reader.hpp | 45 +++++++++++++++++++ single_include/nlohmann/json.hpp | 45 +++++++++++++++++++ tests/src/unit-bson.cpp | 36 +++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index e0a3e2767..e1cca9b5d 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -411,6 +411,11 @@ class binary_reader */ bool get_bson_cstr(string_t& result) { + if (get_bson_cstr_bulk(result, std::integral_constant {})) + { + return true; + } + auto out = std::back_inserter(result); while (true) { @@ -427,6 +432,46 @@ class binary_reader } } + /*! + @brief read a C-style string from contiguous input in one step + + @param[in,out] result the string to append to + @return whether the string was read; if the input has no \x00-byte, nothing + is read, and @ref get_bson_cstr reports the end of the input + */ + bool get_bson_cstr_bulk(string_t& result, std::true_type /*bulk*/) + { + const std::size_t remaining = ia.bulk_remaining(); + if (remaining == 0) + { + return false; + } + const auto* const data = reinterpret_cast(ia.bulk_data()); + // a plain loop rather than std::memchr: most keys are short (array + // indices are keys, too), and the call would cost more than it saves + std::size_t length = 0; + while (length < remaining && data[length] != 0x00) + { + ++length; + } + if (length == remaining) + { + return false; + } + result.append(reinterpret_cast(data), length); + // consume the string and its \x00-byte, as the byte-wise path does + ia.bulk_skip(length + 1); + chars_read += length + 1; + current = 0x00; + return true; + } + + /// input that is not contiguous: C-style strings are read byte by byte + bool get_bson_cstr_bulk(string_t& /*result*/, std::false_type /*bulk*/) const noexcept + { + return false; + } + /*! @brief Parses a zero-terminated string of length @a len from the BSON input. diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 1f9228509..90eecdcd8 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -12930,6 +12930,11 @@ class binary_reader */ bool get_bson_cstr(string_t& result) { + if (get_bson_cstr_bulk(result, std::integral_constant {})) + { + return true; + } + auto out = std::back_inserter(result); while (true) { @@ -12946,6 +12951,46 @@ class binary_reader } } + /*! + @brief read a C-style string from contiguous input in one step + + @param[in,out] result the string to append to + @return whether the string was read; if the input has no \x00-byte, nothing + is read, and @ref get_bson_cstr reports the end of the input + */ + bool get_bson_cstr_bulk(string_t& result, std::true_type /*bulk*/) + { + const std::size_t remaining = ia.bulk_remaining(); + if (remaining == 0) + { + return false; + } + const auto* const data = reinterpret_cast(ia.bulk_data()); + // a plain loop rather than std::memchr: most keys are short (array + // indices are keys, too), and the call would cost more than it saves + std::size_t length = 0; + while (length < remaining && data[length] != 0x00) + { + ++length; + } + if (length == remaining) + { + return false; + } + result.append(reinterpret_cast(data), length); + // consume the string and its \x00-byte, as the byte-wise path does + ia.bulk_skip(length + 1); + chars_read += length + 1; + current = 0x00; + return true; + } + + /// input that is not contiguous: C-style strings are read byte by byte + bool get_bson_cstr_bulk(string_t& /*result*/, std::false_type /*bulk*/) const noexcept + { + return false; + } + /*! @brief Parses a zero-terminated string of length @a len from the BSON input. diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index 669a4bfe1..ec6240388 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -1066,6 +1066,42 @@ TEST_CASE("Incomplete BSON Input") } } +TEST_CASE("BSON keys from contiguous and stream input") +{ + // contiguous input reads a key up to its \x00-byte in one step, a stream + // reads it byte by byte; both must give the same value or error for the + // complete document and for every truncation of it + const json j = {{"", true}, {"k", {1, 2, 3}}, {std::string(40, 'x'), {{"nested key", "value"}}}}; + const std::vector bson = json::to_bson(j); + CHECK(json::from_bson(bson) == j); + + for (std::size_t length = 0; length <= bson.size(); ++length) + { + CAPTURE(length) + const std::vector input(bson.begin(), bson.begin() + static_cast(length)); + std::string from_vector; + std::string from_stream; + try + { + from_vector = json::from_bson(input).dump(); + } + catch (const json::parse_error& e) + { + from_vector = e.what(); + } + try + { + std::istringstream stream(std::string(input.begin(), input.end())); + from_stream = json::from_bson(stream).dump(); + } + catch (const json::parse_error& e) + { + from_stream = e.what(); + } + CHECK(from_vector == from_stream); + } +} + TEST_CASE("Negative size of binary value") { // invalid BSON: the size of the binary value is -1