From 38536b6b4dd1e123e01c4cefb31e1169f3153d5d Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 16 Oct 2026 21:52:38 +0200 Subject: [PATCH] Write the bytes of an unoptimized UBJSON and BON8 binary value in blocks Without use_type, a binary value is an array of its bytes, and was written one sink call per marker and per byte (UBJSON, BJData), or through the integer ladder for each byte (BON8). The bytes are now encoded in blocks into space reserved from the sink, by a function that is not inlined. BON8 stores both bytes of an integer above 39 and keeps the second only if it is needed, so that the code does not branch on the value. Instructions per call for a 1 MB binary value (Linux, GCC 13, thousands): to_ubjson 34253 -> 9521, to_bjdata 34253 -> 9521, to_bon8 53088 -> 17333. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/output/binary_writer.hpp | 98 ++++++++++++++----- single_include/nlohmann/json.hpp | 98 ++++++++++++++----- 2 files changed, 148 insertions(+), 48 deletions(-) diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index d425911b3..324467181 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -8,7 +8,7 @@ #pragma once -#include // reverse +#include // min, reverse #include // array #include // isnan, isinf #include // uint8_t, uint16_t, uint32_t, uint64_t @@ -694,13 +694,7 @@ class binary_writer } else { - for (size_t i = 0; i < j.m_data.m_value.binary->size(); ++i) - { - oa.write_character(to_char_type(bjdata_draft3 ? 'B' : 'U')); - // the cast is needed for binary types whose value type - // is not an integer (e.g., std::byte) - oa.write_character(to_char_type(static_cast(j.m_data.m_value.binary->data()[i]))); - } + write_ubjson_binary_bytes(*j.m_data.m_value.binary, bjdata_draft3); } if (!use_count) @@ -3010,6 +3004,35 @@ class binary_writer /// space for the largest encoding of encode_ubjson_integer(): the marker and 8 bytes static constexpr std::size_t ubjson_integer_encoding_space = 9; + /*! + @brief write the bytes of a binary value as an array of UBJSON uint8 values + + Every byte is written with its marker, in blocks that fit the space reserved + from the sink. Kept out of line, so that write_ubjson does not carry the + registers of its loop. + */ + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_binary_bytes(const binary_t& binary, const bool bjdata_draft3) + { + const CharType marker = to_char_type(bjdata_draft3 ? 'B' : 'U'); + const auto* const data = binary.data(); + const std::size_t size = binary.size(); + for (std::size_t i = 0; i < size;) + { + const std::size_t block = (std::min)(size - i, max_reserve_size / 2); + CharType* const out = oa.reserve(2 * block); + for (std::size_t k = 0; k < block; ++k) + { + out[2 * k] = marker; + // the cast is needed for binary types whose value type + // is not an integer (e.g., std::byte) + out[(2 * k) + 1] = to_char_type(static_cast(data[i + k])); + } + i += block; + oa.commit(2 * block); + } + } + /*! @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 @@ -3748,22 +3771,7 @@ class binary_writer case value_t::binary: { - // BON8 has no binary type: write the bytes as an array of - // integers, like UBJSON and BJData do - const auto N = j.m_data.m_value.binary->size(); - write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); - - for (std::size_t i = 0; i < N; ++i) - { - // the cast is needed for binary types whose value type - // is not an integer (e.g., std::byte) - write_bon8_integer(static_cast(j.m_data.m_value.binary->data()[i])); - } - - if (N > 4) - { - write_bon8_marker(0xFE, string_open); - } + write_bon8_binary(*j.m_data.m_value.binary, string_open); break; } @@ -3850,6 +3858,48 @@ class binary_writer } } + /*! + @brief write a binary value as BON8 + + BON8 has no binary type: the bytes are written as an array of integers, + like UBJSON and BJData do. Kept out of line, so that write_bon8_value does + not carry the registers of its loop. + */ + JSON_HEDLEY_NEVER_INLINE + void write_bon8_binary(const binary_t& binary, bool& string_open) + { + const auto N = binary.size(); + write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); + + // a byte is an integer of 0 to 255: one byte up to 39, two bytes above + const auto* const data = binary.data(); + for (std::size_t i = 0; i < N;) + { + const std::size_t block = (std::min)(N - i, max_reserve_size / 2); + CharType* const start = oa.reserve(2 * block); + CharType* out = start; + for (std::size_t k = 0; k < block; ++k, ++i) + { + // the cast is needed for binary types whose value type + // is not an integer (e.g., std::byte) + const auto value = static_cast(static_cast(data[i])); + // both bytes are stored, the second only counts for a value above 39: + // 0xC2 + ((value - 40) >> 7), (value - 40) & 0x7F + const unsigned wide = value - 40u; + const bool is_wide = value > 39u; + out[0] = to_char_type(static_cast(is_wide ? 0xC2u + (wide >> 7u) : 0x90u + value)); + out[1] = to_char_type(static_cast(wide & 0x7Fu)); + out += is_wide ? 2 : 1; + } + oa.commit(static_cast(out - start)); + } + + if (N > 4) + { + write_bon8_marker(0xFE, string_open); + } + } + /*! @brief write a single byte that is not part of a string diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f472551bf..c1dfa6cf9 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -24241,7 +24241,7 @@ NLOHMANN_JSON_NAMESPACE_END -#include // reverse +#include // min, reverse #include // array #include // isnan, isinf #include // uint8_t, uint16_t, uint32_t, uint64_t @@ -25455,13 +25455,7 @@ class binary_writer } else { - for (size_t i = 0; i < j.m_data.m_value.binary->size(); ++i) - { - oa.write_character(to_char_type(bjdata_draft3 ? 'B' : 'U')); - // the cast is needed for binary types whose value type - // is not an integer (e.g., std::byte) - oa.write_character(to_char_type(static_cast(j.m_data.m_value.binary->data()[i]))); - } + write_ubjson_binary_bytes(*j.m_data.m_value.binary, bjdata_draft3); } if (!use_count) @@ -27771,6 +27765,35 @@ class binary_writer /// space for the largest encoding of encode_ubjson_integer(): the marker and 8 bytes static constexpr std::size_t ubjson_integer_encoding_space = 9; + /*! + @brief write the bytes of a binary value as an array of UBJSON uint8 values + + Every byte is written with its marker, in blocks that fit the space reserved + from the sink. Kept out of line, so that write_ubjson does not carry the + registers of its loop. + */ + JSON_HEDLEY_NEVER_INLINE + void write_ubjson_binary_bytes(const binary_t& binary, const bool bjdata_draft3) + { + const CharType marker = to_char_type(bjdata_draft3 ? 'B' : 'U'); + const auto* const data = binary.data(); + const std::size_t size = binary.size(); + for (std::size_t i = 0; i < size;) + { + const std::size_t block = (std::min)(size - i, max_reserve_size / 2); + CharType* const out = oa.reserve(2 * block); + for (std::size_t k = 0; k < block; ++k) + { + out[2 * k] = marker; + // the cast is needed for binary types whose value type + // is not an integer (e.g., std::byte) + out[(2 * k) + 1] = to_char_type(static_cast(data[i + k])); + } + i += block; + oa.commit(2 * block); + } + } + /*! @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 @@ -28509,22 +28532,7 @@ class binary_writer case value_t::binary: { - // BON8 has no binary type: write the bytes as an array of - // integers, like UBJSON and BJData do - const auto N = j.m_data.m_value.binary->size(); - write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); - - for (std::size_t i = 0; i < N; ++i) - { - // the cast is needed for binary types whose value type - // is not an integer (e.g., std::byte) - write_bon8_integer(static_cast(j.m_data.m_value.binary->data()[i])); - } - - if (N > 4) - { - write_bon8_marker(0xFE, string_open); - } + write_bon8_binary(*j.m_data.m_value.binary, string_open); break; } @@ -28611,6 +28619,48 @@ class binary_writer } } + /*! + @brief write a binary value as BON8 + + BON8 has no binary type: the bytes are written as an array of integers, + like UBJSON and BJData do. Kept out of line, so that write_bon8_value does + not carry the registers of its loop. + */ + JSON_HEDLEY_NEVER_INLINE + void write_bon8_binary(const binary_t& binary, bool& string_open) + { + const auto N = binary.size(); + write_bon8_marker(static_cast(N <= 4 ? 0x80 + N : 0x85), string_open); + + // a byte is an integer of 0 to 255: one byte up to 39, two bytes above + const auto* const data = binary.data(); + for (std::size_t i = 0; i < N;) + { + const std::size_t block = (std::min)(N - i, max_reserve_size / 2); + CharType* const start = oa.reserve(2 * block); + CharType* out = start; + for (std::size_t k = 0; k < block; ++k, ++i) + { + // the cast is needed for binary types whose value type + // is not an integer (e.g., std::byte) + const auto value = static_cast(static_cast(data[i])); + // both bytes are stored, the second only counts for a value above 39: + // 0xC2 + ((value - 40) >> 7), (value - 40) & 0x7F + const unsigned wide = value - 40u; + const bool is_wide = value > 39u; + out[0] = to_char_type(static_cast(is_wide ? 0xC2u + (wide >> 7u) : 0x90u + value)); + out[1] = to_char_type(static_cast(wide & 0x7Fu)); + out += is_wide ? 2 : 1; + } + oa.commit(static_cast(out - start)); + } + + if (N > 4) + { + write_bon8_marker(0xFE, string_open); + } + } + /*! @brief write a single byte that is not part of a string