From a517b8ea8bba732551aebf3aab655c279e3391b5 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 16 Oct 2026 22:18:20 +0200 Subject: [PATCH] Write a UBJSON, BJData or BSON string and its head in one reservation Same as for CBOR and MessagePack: with the default error handler (keep) a string or object key of up to 253 characters (UBJSON, BJData: the `S` marker, the length as `i` or `U` and one byte, and the characters), or a BSON string entry that fits the reservation limit (type, name, length, characters and null byte), is written into space reserved from the sink in one go, instead of by four to six sink writes, a string_t for the error handler, and a call of sanitize_utf8_for_write(). The name of a BSON entry is copied with the short- string copy of the sinks. Instructions per call (Linux, GCC 13, thousands), to_ubjson: 20000 small objects 28241 -> 20981, twitter 5175 -> 3867, citm_catalog 10681 -> 8857; to_bjdata: 20000 small objects 28261 -> 21001, twitter 5181 -> 3873. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/output/binary_writer.hpp | 91 ++++++++++++++----- single_include/nlohmann/json.hpp | 91 ++++++++++++++----- 2 files changed, 132 insertions(+), 50 deletions(-) diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 9ca47cb7a..8fe3ba98d 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -621,17 +621,7 @@ class binary_writer case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - if (add_prefix) - { - oa.write_character(to_char_type('S')); - } - write_number_with_ubjson_prefix(value.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_ubjson_text(*j.m_data.m_value.string, j, add_prefix, use_bjdata); break; } @@ -722,12 +712,7 @@ class binary_writer { // a converted key must outlive the reference returned by sanitize_utf8_for_write const object_key_string_t key_string = el.first; - string_t storage; - const string_t& key = sanitize_utf8_for_write(key_string, j, storage); - write_number_with_ubjson_prefix(key.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(key.data()), - key.size()); + write_ubjson_text(key_string, j, false, use_bjdata); write_ubjson(el.second, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); } @@ -1403,12 +1388,7 @@ class binary_writer // a converted key must outlive the reference returned by sanitize_utf8_for_write const object_key_string_t key_string = current.object_it->first; - string_t storage; - const string_t& key = sanitize_utf8_for_write(key_string, j, storage); - write_number_with_ubjson_prefix(key.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(key.data()), - key.size()); + write_ubjson_text(key_string, j, false, use_bjdata); const BasicJsonType* child = &(current.object_it->second); const bool child_prefix = current.prefix_required; ++stack.back().object_it; @@ -1893,10 +1873,13 @@ class binary_writer bool write_bson_scalar(const string_t& name, const BasicJsonType& j) { const std::size_t name_size = name.size(); - CharType* const out = oa.reserve(name_size + 2 + 8); + // a short string is written whole: its type, the name and its null + // byte, the length, the characters, and their null byte + const bool whole_string = j.is_string() && j.m_data.m_value.string->size() <= max_reserve_size - name_size - 7; + CharType* const out = oa.reserve(whole_string ? name_size + j.m_data.m_value.string->size() + 7 : name_size + 2 + 8); // the name and its terminating null byte follow the type byte CharType* p = out + 1; - std::memcpy(p, name.data(), name_size); + copy_characters(p, reinterpret_cast(name.data()), name_size); p += name_size; *p++ = to_char_type(0x00); @@ -1906,6 +1889,22 @@ class binary_writer return true; } + if (whole_string) + { + const string_t& value = *j.m_data.m_value.string; + out[0] = to_char_type(0x02); + store_little_endian(p, static_cast(value.size() + 1ul)); + p += sizeof(std::int32_t); + copy_characters(p, reinterpret_cast(value.data()), value.size()); + p += value.size(); + // the terminating null byte is written explicitly rather than + // taken from the buffer, so that string_t::data() need not be + // null-terminated + *p++ = to_char_type(0x00); + oa.commit(static_cast(p - out)); + return true; + } + if (j.is_string()) { const string_t& value = *j.m_data.m_value.string; @@ -3138,6 +3137,48 @@ class binary_writer } } + /*! + @brief write a UBJSON/BJData string or object key: the optional string + marker `S`, the length as an integer with its marker, and the characters + + With the default error handler (keep) and a string of up to 253 characters, + which has a length of the marker `i` or `U` and a single byte, everything is + written into space reserved from the sink in one go. + + @param[in] value the string; checked or sanitized according to + @ref error_handler, with @a context as diagnostics context + @param[in] add_marker whether `S` is written first (not for an object key) + */ + void write_ubjson_text(const string_t& value, const BasicJsonType& context, const bool add_marker, const bool use_bjdata) + { + const std::size_t size = value.size(); + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep) && JSON_HEDLEY_LIKELY(size <= max_text_in_reservation - 1)) + { + CharType* const out = oa.reserve(size + 3); + CharType* p = out; + if (add_marker) + { + *p++ = to_char_type('S'); + } + *p++ = to_char_type(size <= 127 ? 'i' : 'U'); + *p++ = to_char_type(static_cast(size)); + copy_characters(p, reinterpret_cast(value.data()), size); + oa.commit(static_cast(p - out) + size); + return; + } + + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + if (add_marker) + { + oa.write_character(to_char_type('S')); + } + write_number_with_ubjson_prefix(sanitized.size(), true, use_bjdata); + oa.write_characters( + reinterpret_cast(sanitized.data()), + sanitized.size()); + } + /*! @brief encode a UBJSON/BJData float, with its marker if @a add_prefix @param[in] out where the encoding goes; at most 9 characters are written diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 718060947..bcf704525 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -25449,17 +25449,7 @@ class binary_writer case value_t::string: { - string_t storage; - const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage); - - if (add_prefix) - { - oa.write_character(to_char_type('S')); - } - write_number_with_ubjson_prefix(value.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(value.data()), - value.size()); + write_ubjson_text(*j.m_data.m_value.string, j, add_prefix, use_bjdata); break; } @@ -25550,12 +25540,7 @@ class binary_writer { // a converted key must outlive the reference returned by sanitize_utf8_for_write const object_key_string_t key_string = el.first; - string_t storage; - const string_t& key = sanitize_utf8_for_write(key_string, j, storage); - write_number_with_ubjson_prefix(key.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(key.data()), - key.size()); + write_ubjson_text(key_string, j, false, use_bjdata); write_ubjson(el.second, use_count, use_type, prefix_required, use_bjdata, bjdata_version, depth + 1); } @@ -26231,12 +26216,7 @@ class binary_writer // a converted key must outlive the reference returned by sanitize_utf8_for_write const object_key_string_t key_string = current.object_it->first; - string_t storage; - const string_t& key = sanitize_utf8_for_write(key_string, j, storage); - write_number_with_ubjson_prefix(key.size(), true, use_bjdata); - oa.write_characters( - reinterpret_cast(key.data()), - key.size()); + write_ubjson_text(key_string, j, false, use_bjdata); const BasicJsonType* child = &(current.object_it->second); const bool child_prefix = current.prefix_required; ++stack.back().object_it; @@ -26721,10 +26701,13 @@ class binary_writer bool write_bson_scalar(const string_t& name, const BasicJsonType& j) { const std::size_t name_size = name.size(); - CharType* const out = oa.reserve(name_size + 2 + 8); + // a short string is written whole: its type, the name and its null + // byte, the length, the characters, and their null byte + const bool whole_string = j.is_string() && j.m_data.m_value.string->size() <= max_reserve_size - name_size - 7; + CharType* const out = oa.reserve(whole_string ? name_size + j.m_data.m_value.string->size() + 7 : name_size + 2 + 8); // the name and its terminating null byte follow the type byte CharType* p = out + 1; - std::memcpy(p, name.data(), name_size); + copy_characters(p, reinterpret_cast(name.data()), name_size); p += name_size; *p++ = to_char_type(0x00); @@ -26734,6 +26717,22 @@ class binary_writer return true; } + if (whole_string) + { + const string_t& value = *j.m_data.m_value.string; + out[0] = to_char_type(0x02); + store_little_endian(p, static_cast(value.size() + 1ul)); + p += sizeof(std::int32_t); + copy_characters(p, reinterpret_cast(value.data()), value.size()); + p += value.size(); + // the terminating null byte is written explicitly rather than + // taken from the buffer, so that string_t::data() need not be + // null-terminated + *p++ = to_char_type(0x00); + oa.commit(static_cast(p - out)); + return true; + } + if (j.is_string()) { const string_t& value = *j.m_data.m_value.string; @@ -27966,6 +27965,48 @@ class binary_writer } } + /*! + @brief write a UBJSON/BJData string or object key: the optional string + marker `S`, the length as an integer with its marker, and the characters + + With the default error handler (keep) and a string of up to 253 characters, + which has a length of the marker `i` or `U` and a single byte, everything is + written into space reserved from the sink in one go. + + @param[in] value the string; checked or sanitized according to + @ref error_handler, with @a context as diagnostics context + @param[in] add_marker whether `S` is written first (not for an object key) + */ + void write_ubjson_text(const string_t& value, const BasicJsonType& context, const bool add_marker, const bool use_bjdata) + { + const std::size_t size = value.size(); + if (JSON_HEDLEY_LIKELY(error_handler == error_handler_t::keep) && JSON_HEDLEY_LIKELY(size <= max_text_in_reservation - 1)) + { + CharType* const out = oa.reserve(size + 3); + CharType* p = out; + if (add_marker) + { + *p++ = to_char_type('S'); + } + *p++ = to_char_type(size <= 127 ? 'i' : 'U'); + *p++ = to_char_type(static_cast(size)); + copy_characters(p, reinterpret_cast(value.data()), size); + oa.commit(static_cast(p - out) + size); + return; + } + + string_t storage; + const string_t& sanitized = sanitize_utf8_for_write(value, context, storage); + if (add_marker) + { + oa.write_character(to_char_type('S')); + } + write_number_with_ubjson_prefix(sanitized.size(), true, use_bjdata); + oa.write_characters( + reinterpret_cast(sanitized.data()), + sanitized.size()); + } + /*! @brief encode a UBJSON/BJData float, with its marker if @a add_prefix @param[in] out where the encoding goes; at most 9 characters are written