From 135095d692c08a0577428313f85a2b93bfa666b8 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 16 Oct 2026 21:22:59 +0200 Subject: [PATCH] Name BSON array elements without a std::string per element BSON stores an array as a document whose keys are the decimal indices. Both passes built that key with std::to_string() and copied it into a string_t for every element, and the size pass then validated it like an object key (a search for U+0000 and the UTF-8 handler), though a decimal number cannot fail either. The index name now lives in the array's frame as digits that are incremented in place, and the size pass adds 2 + the number of digits. Instructions per call (Linux, GCC 13, thousands), to_bson: 20000 integers 16228 -> 6486, 20000 floats 16304 -> 6562, canada 109155 -> 55282, 20000 small objects 45513 -> 37171, citm_catalog 17269 -> 15026. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/output/binary_writer.hpp | 52 +++++++++++-------- single_include/nlohmann/json.hpp | 52 +++++++++++-------- 2 files changed, 62 insertions(+), 42 deletions(-) diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 916307b89..7e79d4be3 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -1770,30 +1770,43 @@ class binary_writer } } + /// make @ref index the next one: count it and update its decimal name + void next_index() noexcept + { + ++index; + std::size_t i = index_name_size; + while (i > 0) + { + --i; + if (index_name[i] != '9') + { + ++index_name[i]; + return; + } + index_name[i] = '0'; + } + // all digits were 9: 10, 100, ... + index_name[0] = '1'; + index_name[index_name_size++] = '0'; + } + /// the object or array const BasicJsonType* value; /// objects: the next member typename BasicJsonType::object_t::const_iterator member{}; /// arrays: the index of the next element std::size_t index = 0; + /// arrays: the name BSON gives that element, its decimal index (the + /// index is at most 2^64 - 1, which has 20 digits) + std::array index_name{{'0'}}; + /// arrays: the number of characters of @ref index_name + std::size_t index_name_size = 1; /// @ref calc_bson_sizes only: where its size goes in the table std::size_t size_slot; /// @ref calc_bson_sizes only: the size of its entries seen so far std::size_t entries_size = 0; }; - /*! - @brief creates the name BSON gives the array element with index @a index - @param[out] name receives the decimal index - */ - static void create_bson_index_name(const std::size_t index, string_t& name) - { - // the index is built as a std::string; convert explicitly, as the - // two are only implicitly convertible for some string types - const auto key = std::to_string(index); - name = string_t(key.data(), key.size()); - } - /*! @brief Calculates the size of every object and array in the BSON document @a document, including the document itself. @@ -1825,8 +1838,6 @@ class binary_writer // is in; nothing is allocated unless the document nests bson_frame current(&document); std::vector parents; - // string_t need not be default constructible - string_t index_name("", 0); while (true) { @@ -1857,9 +1868,10 @@ class binary_writer while (nested == nullptr && current.index < array.size()) { const BasicJsonType& el = array[current.index]; - create_bson_index_name(current.index, index_name); - current.entries_size += calc_bson_entry_header_size(index_name, el); - ++current.index; + // the type, the decimal index (which is valid UTF-8 and + // has no U+0000), and the terminating null byte + current.entries_size += 2ul + current.index_name_size; + current.next_index(); if (el.is_structured()) { nested = ⪙ @@ -1914,8 +1926,6 @@ class binary_writer bson_frame current(&document); std::vector parents; std::size_t next_size = 0; - // string_t need not be default constructible - string_t index_name("", 0); while (true) { @@ -1946,8 +1956,8 @@ class binary_writer while (nested == nullptr && current.index < array.size()) { const BasicJsonType& el = array[current.index]; - create_bson_index_name(current.index, index_name); - ++current.index; + const string_t index_name(current.index_name.data(), current.index_name_size); + current.next_index(); if (el.is_structured()) { write_bson_nested_head(index_name, el, nested_sizes[next_size++]); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 89e89c12a..679c0ea97 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -26439,30 +26439,43 @@ class binary_writer } } + /// make @ref index the next one: count it and update its decimal name + void next_index() noexcept + { + ++index; + std::size_t i = index_name_size; + while (i > 0) + { + --i; + if (index_name[i] != '9') + { + ++index_name[i]; + return; + } + index_name[i] = '0'; + } + // all digits were 9: 10, 100, ... + index_name[0] = '1'; + index_name[index_name_size++] = '0'; + } + /// the object or array const BasicJsonType* value; /// objects: the next member typename BasicJsonType::object_t::const_iterator member{}; /// arrays: the index of the next element std::size_t index = 0; + /// arrays: the name BSON gives that element, its decimal index (the + /// index is at most 2^64 - 1, which has 20 digits) + std::array index_name{{'0'}}; + /// arrays: the number of characters of @ref index_name + std::size_t index_name_size = 1; /// @ref calc_bson_sizes only: where its size goes in the table std::size_t size_slot; /// @ref calc_bson_sizes only: the size of its entries seen so far std::size_t entries_size = 0; }; - /*! - @brief creates the name BSON gives the array element with index @a index - @param[out] name receives the decimal index - */ - static void create_bson_index_name(const std::size_t index, string_t& name) - { - // the index is built as a std::string; convert explicitly, as the - // two are only implicitly convertible for some string types - const auto key = std::to_string(index); - name = string_t(key.data(), key.size()); - } - /*! @brief Calculates the size of every object and array in the BSON document @a document, including the document itself. @@ -26494,8 +26507,6 @@ class binary_writer // is in; nothing is allocated unless the document nests bson_frame current(&document); std::vector parents; - // string_t need not be default constructible - string_t index_name("", 0); while (true) { @@ -26526,9 +26537,10 @@ class binary_writer while (nested == nullptr && current.index < array.size()) { const BasicJsonType& el = array[current.index]; - create_bson_index_name(current.index, index_name); - current.entries_size += calc_bson_entry_header_size(index_name, el); - ++current.index; + // the type, the decimal index (which is valid UTF-8 and + // has no U+0000), and the terminating null byte + current.entries_size += 2ul + current.index_name_size; + current.next_index(); if (el.is_structured()) { nested = ⪙ @@ -26583,8 +26595,6 @@ class binary_writer bson_frame current(&document); std::vector parents; std::size_t next_size = 0; - // string_t need not be default constructible - string_t index_name("", 0); while (true) { @@ -26615,8 +26625,8 @@ class binary_writer while (nested == nullptr && current.index < array.size()) { const BasicJsonType& el = array[current.index]; - create_bson_index_name(current.index, index_name); - ++current.index; + const string_t index_name(current.index_name.data(), current.index_name_size); + current.next_index(); if (el.is_structured()) { write_bson_nested_head(index_name, el, nested_sizes[next_size++]);