From eb251df17c0c9d7664ffaab17f599b88b9eab364 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 16 Oct 2026 22:35:54 +0200 Subject: [PATCH] Size and write BSON arrays of numbers without a frame on the stack Every array nested in a BSON document pushed a frame of about 100 bytes on a vector, replaced the current one by moving, and popped it again, once for the size pass and once for the write pass. For the many small arrays of numbers (coordinates, vectors) that was more than the work on their entries. An array whose elements are all booleans, numbers, or null is now sized by a loop of its own, and written in the loop for fixed-size entries on a frame that is not pushed. The entries of other arrays go to the fixed-size loop only when the element is such a value, which also saves the call for an array of strings. An array with an integer that does not fit 64 bits (possible with number types wider than that) is left to the generic path, which throws. Instructions per call (Linux, GCC 13, thousands), to_bson: canada 46833 -> 30575, 20000 integers 2680 -> 1839, 20000 floats 3050 -> 2209, citm_catalog 14536 -> 13235, 20000 small objects 33294 -> 31954. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/output/binary_writer.hpp | 100 +++++++++++++++++- single_include/nlohmann/json.hpp | 100 +++++++++++++++++- 2 files changed, 194 insertions(+), 6 deletions(-) diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 75a9b6f1e..f42c10c75 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -2081,6 +2081,13 @@ class binary_writer std::size_t entries_size = 0; }; + /// whether the BSON entry of @a j has a fixed size, which is the case for + /// a boolean, a number, and null + static bool is_bson_fixed_size(const BasicJsonType& j) noexcept + { + return j.is_number() || j.is_boolean() || j.is_null(); + } + /*! @brief Writes the entries of the array in @a frame that have a fixed size (booleans, numbers, and null), from its next element on, until it @@ -2151,6 +2158,67 @@ class binary_writer frame.index_name = name; } + /*! + @brief Calculates the size of the BSON array @a j if all its elements are + booleans, numbers, or null + @param[out] size the size of the array, including its own size field + @return false (and nothing is known about @a size) if an element is not, + or is an integer that does not fit 64 bits (which the generic + path rejects), which is not looked at further + */ + bool calc_bson_fixed_array_size(const BasicJsonType& j, std::size_t& size) const noexcept + { + std::size_t total = sizeof(std::int32_t) + 1ul; + std::size_t index_name_size = 1; + std::uint64_t index_boundary = 10; + std::uint64_t index = 0; + for (const auto& el : *j.m_data.m_value.array) + { + // the type, the decimal index, and the terminating null byte + total += 2ul + index_name_size; + if (++index == index_boundary) + { + ++index_name_size; + index_boundary *= 10; + } + switch (el.type()) + { + case value_t::boolean: + total += 1ul; + break; + case value_t::number_float: + total += 8ul; + break; + case value_t::number_integer: + // an integer beyond 64 bits is left to the generic path, which throws + if (JSON_HEDLEY_UNLIKELY(!integer_fits(el.m_data.m_value.number_integer))) + { + return false; + } + total += calc_bson_integer_size(static_cast(el.m_data.m_value.number_integer)); + break; + case value_t::number_unsigned: + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(el.m_data.m_value.number_unsigned))) + { + return false; + } + total += calc_bson_unsigned_size(conditional_static_cast(el.m_data.m_value.number_unsigned)); + break; + case value_t::null: + break; + case value_t::string: + case value_t::binary: + case value_t::object: + case value_t::array: + case value_t::discarded: + default: + return false; + } + } + size = total; + return true; + } + /*! @brief Calculates the size of every object and array in the BSON document @a document, including the document itself. @@ -2229,6 +2297,15 @@ class binary_writer if (nested != nullptr) { + // an array of booleans, numbers and null: sized without a frame + std::size_t fixed_size = 0; + if (nested->is_array() && calc_bson_fixed_array_size(*nested, fixed_size)) + { + nested_sizes.push_back(fixed_size); + current.entries_size += fixed_size; + continue; + } + // its size is added to the current one's once it is done nested_sizes.push_back(0); parents.push_back(std::move(current)); @@ -2299,10 +2376,13 @@ class binary_writer const auto& array = *current.value->m_data.m_value.array; while (nested == nullptr && current.index < array.size()) { - write_bson_array_fixed_entries(current); - if (current.index == array.size()) + if (is_bson_fixed_size(array[current.index])) { - break; + write_bson_array_fixed_entries(current); + if (current.index == array.size()) + { + break; + } } // a string, binary value, object or array @@ -2323,6 +2403,20 @@ class binary_writer if (nested != nullptr) { + if (nested->is_array()) + { + // an array of booleans, numbers and null is written without a frame on the stack + bson_frame leaf(nested); + write_bson_array_fixed_entries(leaf); + if (leaf.index == nested->m_data.m_value.array->size()) + { + oa.write_character(to_char_type(0x00)); + continue; + } + parents.push_back(std::move(current)); + current = std::move(leaf); + continue; + } parents.push_back(std::move(current)); current = bson_frame(nested); continue; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index bacedf9d5..3cbe6cebd 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -26909,6 +26909,13 @@ class binary_writer std::size_t entries_size = 0; }; + /// whether the BSON entry of @a j has a fixed size, which is the case for + /// a boolean, a number, and null + static bool is_bson_fixed_size(const BasicJsonType& j) noexcept + { + return j.is_number() || j.is_boolean() || j.is_null(); + } + /*! @brief Writes the entries of the array in @a frame that have a fixed size (booleans, numbers, and null), from its next element on, until it @@ -26979,6 +26986,67 @@ class binary_writer frame.index_name = name; } + /*! + @brief Calculates the size of the BSON array @a j if all its elements are + booleans, numbers, or null + @param[out] size the size of the array, including its own size field + @return false (and nothing is known about @a size) if an element is not, + or is an integer that does not fit 64 bits (which the generic + path rejects), which is not looked at further + */ + bool calc_bson_fixed_array_size(const BasicJsonType& j, std::size_t& size) const noexcept + { + std::size_t total = sizeof(std::int32_t) + 1ul; + std::size_t index_name_size = 1; + std::uint64_t index_boundary = 10; + std::uint64_t index = 0; + for (const auto& el : *j.m_data.m_value.array) + { + // the type, the decimal index, and the terminating null byte + total += 2ul + index_name_size; + if (++index == index_boundary) + { + ++index_name_size; + index_boundary *= 10; + } + switch (el.type()) + { + case value_t::boolean: + total += 1ul; + break; + case value_t::number_float: + total += 8ul; + break; + case value_t::number_integer: + // an integer beyond 64 bits is left to the generic path, which throws + if (JSON_HEDLEY_UNLIKELY(!integer_fits(el.m_data.m_value.number_integer))) + { + return false; + } + total += calc_bson_integer_size(static_cast(el.m_data.m_value.number_integer)); + break; + case value_t::number_unsigned: + if (JSON_HEDLEY_UNLIKELY(!integer_fits_max(el.m_data.m_value.number_unsigned))) + { + return false; + } + total += calc_bson_unsigned_size(conditional_static_cast(el.m_data.m_value.number_unsigned)); + break; + case value_t::null: + break; + case value_t::string: + case value_t::binary: + case value_t::object: + case value_t::array: + case value_t::discarded: + default: + return false; + } + } + size = total; + return true; + } + /*! @brief Calculates the size of every object and array in the BSON document @a document, including the document itself. @@ -27057,6 +27125,15 @@ class binary_writer if (nested != nullptr) { + // an array of booleans, numbers and null: sized without a frame + std::size_t fixed_size = 0; + if (nested->is_array() && calc_bson_fixed_array_size(*nested, fixed_size)) + { + nested_sizes.push_back(fixed_size); + current.entries_size += fixed_size; + continue; + } + // its size is added to the current one's once it is done nested_sizes.push_back(0); parents.push_back(std::move(current)); @@ -27127,10 +27204,13 @@ class binary_writer const auto& array = *current.value->m_data.m_value.array; while (nested == nullptr && current.index < array.size()) { - write_bson_array_fixed_entries(current); - if (current.index == array.size()) + if (is_bson_fixed_size(array[current.index])) { - break; + write_bson_array_fixed_entries(current); + if (current.index == array.size()) + { + break; + } } // a string, binary value, object or array @@ -27151,6 +27231,20 @@ class binary_writer if (nested != nullptr) { + if (nested->is_array()) + { + // an array of booleans, numbers and null is written without a frame on the stack + bson_frame leaf(nested); + write_bson_array_fixed_entries(leaf); + if (leaf.index == nested->m_data.m_value.array->size()) + { + oa.write_character(to_char_type(0x00)); + continue; + } + parents.push_back(std::move(current)); + current = std::move(leaf); + continue; + } parents.push_back(std::move(current)); current = bson_frame(nested); continue;