From e82ee7375a320c168a0bb1ee58a5c3573046f4ea Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 16 Oct 2026 21:33:54 +0200 Subject: [PATCH] Collect the writes of the adapter sink and pass them on in blocks to_cbor(j, o) and its siblings with a std::string, a std::vector or a std::ostream go through output_adapter_sink, where every write is a virtual call, and for a stream a call with a sentry on top. That costs more than encoding the few bytes of most values, which is why those overloads were 1.8 to 3 times slower than the vector-returning ones. The sink now keeps a 1 KiB buffer and passes it on when it is full, when a write is larger than the space left (a payload of a KiB or more goes directly), or when binary_writer::flush() is called at the end of the to_* function. Everything reaches the adapter in the same order. The destructor flushes what is left, so a value that throws (invalid UTF-8 with the strict handler) leaves the same partial output as before; only flush() can report an error of the adapter, as a destructor must not throw. Instructions per call (Linux, GCC 13, thousands), to_*(j, std::string&): twitter cbor 4952 -> 3356, twitter ubjson 6762 -> 4647, twitter bson 5518 -> 4704, citm_catalog cbor 10404 -> 7090; to_*(j, std::ostream&): twitter cbor 9178 -> 3844, twitter ubjson 11974 -> 5129, citm_catalog cbor 18374 -> 7554. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/output/binary_writer.hpp | 15 +- .../detail/output/output_adapters.hpp | 116 ++++++++++-- include/nlohmann/json.hpp | 48 +++-- single_include/nlohmann/json.hpp | 179 +++++++++++++++--- 4 files changed, 308 insertions(+), 50 deletions(-) diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 2d7c98c61..200e239f4 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -123,9 +123,22 @@ class binary_writer template < typename SinkType = OutputSinkType, typename std::enable_if < std::is_constructible>::value, int >::type = 0 > explicit binary_writer(output_adapter_t adapter, const error_handler_t error_handler_ = binary_writer_default_error_handler()) - : oa(SinkType(std::move(adapter))), error_handler(error_handler_) + : oa(std::move(adapter)), error_handler(error_handler_) {} + /*! + @brief pass on everything written so far to the output adapter + + A writer over an output_adapter_sink buffers what it writes; this has to + be called after the last write, or the buffered end is only passed on, and + an error of the adapter lost, when the writer is destroyed. A writer over + an output_vector_sink writes straight into the vector. + */ + void flush() + { + oa.flush(); + } + /*! @param[in] j JSON value to serialize @throw type_error.316 if a string value or an object key is not valid diff --git a/include/nlohmann/detail/output/output_adapters.hpp b/include/nlohmann/detail/output/output_adapters.hpp index f9aa76688..917c8e648 100644 --- a/include/nlohmann/detail/output/output_adapters.hpp +++ b/include/nlohmann/detail/output/output_adapters.hpp @@ -158,6 +158,10 @@ class output_vector_sink position += length; } + /// nothing to pass on: the characters are in the vector (see output_adapter_sink) + void flush() noexcept + {} + private: /// make room for at least @a length more characters after the position; /// kept out of line so that the write functions stay small enough for @@ -277,9 +281,17 @@ class output_string_adapter : public output_adapter_protocol /// /// Wraps the polymorphic output_adapter_t so the same binary_writer template can /// also target arbitrary adapters (output streams, strings, user-provided -/// adapters) via the `output_adapter`-based overloads. Each write still goes -/// through one virtual call, exactly as before; only the concrete sinks above -/// avoid it. +/// adapters) via the `output_adapter`-based overloads. +/// +/// A write through the adapter is a virtual call, and for a stream a call with +/// a sentry on top, which costs more than encoding the few bytes of most values. +/// The sink therefore collects what is written in a buffer and passes it on in +/// one piece when it is full, a write is too large for it, or @ref flush is +/// called. Everything written reaches the adapter in the same order and with +/// the same bytes; the destructor passes on what is left, so that a write that +/// throws leaves the adapter with exactly the bytes written before it. Only +/// @ref flush can report an error of the adapter (the destructor must not +/// throw), so it has to be called once the value is written. template class output_adapter_sink { @@ -290,36 +302,116 @@ class output_adapter_sink JSON_ASSERT(oa); } + output_adapter_sink(const output_adapter_sink&) = delete; + output_adapter_sink& operator=(const output_adapter_sink&) = delete; + output_adapter_sink& operator=(output_adapter_sink&&) = delete; + + output_adapter_sink(output_adapter_sink&& other) noexcept + : oa(std::move(other.oa)), used(other.used) + { + std::memcpy(buffer.data(), other.buffer.data(), other.used * sizeof(CharType)); + other.used = 0; + } + + ~output_adapter_sink() + { + // a normal return has flushed already, so this is for a write that threw + if (used != 0) + { + JSON_TRY + { + flush(); + } + JSON_CATCH(...) + { + // nothing can be done about an adapter that throws here + } + } + } + + JSON_HEDLEY_ALWAYS_INLINE void write_character(CharType c) { - oa->write_character(c); + if (JSON_HEDLEY_UNLIKELY(used == buffer_size)) + { + flush(); + } + buffer[used++] = c; } // no JSON_HEDLEY_NON_NULL: forwards (null, 0) for empty payloads, exactly as // the type-erased path already did before this sink existed + JSON_HEDLEY_ALWAYS_INLINE void write_characters(const CharType* s, std::size_t length) { - oa->write_characters(s, length); + if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length)) + { + write_large(s, length); + return; + } + if (length != 0) + { + std::memcpy(buffer.data() + used, s, length * sizeof(CharType)); + used += length; + } } /// space for writing up to @a length characters (see /// output_vector_sink::reserve) - CharType* reserve(const std::size_t length) noexcept + JSON_HEDLEY_ALWAYS_INLINE + CharType* reserve(const std::size_t length) { JSON_ASSERT(length <= max_reserve_size); - static_cast(length); - return reserved.data(); + if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length)) + { + flush(); + } + return buffer.data() + used; } - /// write the first @a length characters written to the space from reserve() - void commit(const std::size_t length) + /// keep the first @a length characters written to the space from reserve() + JSON_HEDLEY_ALWAYS_INLINE + void commit(const std::size_t length) noexcept { - oa->write_characters(reserved.data(), length); + used += length; + } + + /// pass everything written so far on to the adapter + void flush() + { + if (used != 0) + { + // reset first: the characters count as written even if the adapter throws + const std::size_t length = used; + used = 0; + oa->write_characters(buffer.data(), length); + } } private: + /// a write that does not fit the space left in the buffer + JSON_HEDLEY_NEVER_INLINE + void write_large(const CharType* s, const std::size_t length) + { + flush(); + if (length >= buffer_size) + { + oa->write_characters(s, length); + } + else + { + std::memcpy(buffer.data(), s, length * sizeof(CharType)); + used = length; + } + } + + /// large enough for reserve() at any time + static constexpr std::size_t buffer_size = 4 * max_reserve_size; + output_adapter_t oa; - std::array reserved{}; + /// number of characters of @ref buffer not yet passed on + std::size_t used = 0; + std::array buffer{}; }; /// @brief whether std::basic_string has a non-deprecated std::char_traits diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 9c0e11632..752d1a3bc 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -6331,7 +6331,9 @@ public: static void to_cbor(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_cbor(j); + binary_writer writer(o, error_handler); + writer.write_cbor(j); + writer.flush(); } /// @brief create a CBOR serialization of a given JSON value @@ -6339,7 +6341,9 @@ public: static void to_cbor(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_cbor(j); + binary_writer writer(o, error_handler); + writer.write_cbor(j); + writer.flush(); } /// @brief create a MessagePack serialization of a given JSON value @@ -6358,7 +6362,9 @@ public: static void to_msgpack(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = error_handler_t::keep) { - binary_writer(o, error_handler).write_msgpack(j); + binary_writer writer(o, error_handler); + writer.write_msgpack(j); + writer.flush(); } /// @brief create a MessagePack serialization of a given JSON value @@ -6366,7 +6372,9 @@ public: static void to_msgpack(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = error_handler_t::keep) { - binary_writer(o, error_handler).write_msgpack(j); + binary_writer writer(o, error_handler); + writer.write_msgpack(j); + writer.flush(); } /// @brief create a UBJSON serialization of a given JSON value @@ -6388,7 +6396,9 @@ public: const bool use_size = false, const bool use_type = false, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type); + writer.flush(); } /// @brief create a UBJSON serialization of a given JSON value @@ -6397,7 +6407,9 @@ public: const bool use_size = false, const bool use_type = false, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type); + writer.flush(); } /// @brief create a BJData serialization of a given JSON value @@ -6421,7 +6433,9 @@ public: const bjdata_version_t version = bjdata_version_t::draft2, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type, true, true, version); + writer.flush(); } /// @brief create a BJData serialization of a given JSON value @@ -6431,7 +6445,9 @@ public: const bjdata_version_t version = bjdata_version_t::draft2, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type, true, true, version); + writer.flush(); } /// @brief create a BSON serialization of a given JSON value @@ -6450,7 +6466,9 @@ public: static void to_bson(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_bson(j); + binary_writer writer(o, error_handler); + writer.write_bson(j); + writer.flush(); } /// @brief create a BSON serialization of a given JSON value @@ -6458,7 +6476,9 @@ public: static void to_bson(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_bson(j); + binary_writer writer(o, error_handler); + writer.write_bson(j); + writer.flush(); } /// @brief create a BON8 serialization of a given JSON value @@ -6475,14 +6495,18 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ static void to_bon8(const basic_json& j, detail::output_adapter o) { - binary_writer(o).write_bon8(j); + binary_writer writer(o); + writer.write_bon8(j); + writer.flush(); } /// @brief create a BON8 serialization of a given JSON value /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ static void to_bon8(const basic_json& j, detail::output_adapter o) { - binary_writer(o).write_bon8(j); + binary_writer writer(o); + writer.write_bon8(j); + writer.flush(); } /// @brief create a JSON value from an input in CBOR format diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 2384a1bb3..49d05d844 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -24424,6 +24424,10 @@ class output_vector_sink position += length; } + /// nothing to pass on: the characters are in the vector (see output_adapter_sink) + void flush() noexcept + {} + private: /// make room for at least @a length more characters after the position; /// kept out of line so that the write functions stay small enough for @@ -24543,9 +24547,17 @@ class output_string_adapter : public output_adapter_protocol /// /// Wraps the polymorphic output_adapter_t so the same binary_writer template can /// also target arbitrary adapters (output streams, strings, user-provided -/// adapters) via the `output_adapter`-based overloads. Each write still goes -/// through one virtual call, exactly as before; only the concrete sinks above -/// avoid it. +/// adapters) via the `output_adapter`-based overloads. +/// +/// A write through the adapter is a virtual call, and for a stream a call with +/// a sentry on top, which costs more than encoding the few bytes of most values. +/// The sink therefore collects what is written in a buffer and passes it on in +/// one piece when it is full, a write is too large for it, or @ref flush is +/// called. Everything written reaches the adapter in the same order and with +/// the same bytes; the destructor passes on what is left, so that a write that +/// throws leaves the adapter with exactly the bytes written before it. Only +/// @ref flush can report an error of the adapter (the destructor must not +/// throw), so it has to be called once the value is written. template class output_adapter_sink { @@ -24556,36 +24568,116 @@ class output_adapter_sink JSON_ASSERT(oa); } + output_adapter_sink(const output_adapter_sink&) = delete; + output_adapter_sink& operator=(const output_adapter_sink&) = delete; + output_adapter_sink& operator=(output_adapter_sink&&) = delete; + + output_adapter_sink(output_adapter_sink&& other) noexcept + : oa(std::move(other.oa)), used(other.used) + { + std::memcpy(buffer.data(), other.buffer.data(), other.used * sizeof(CharType)); + other.used = 0; + } + + ~output_adapter_sink() + { + // a normal return has flushed already, so this is for a write that threw + if (used != 0) + { + JSON_TRY + { + flush(); + } + JSON_CATCH(...) + { + // nothing can be done about an adapter that throws here + } + } + } + + JSON_HEDLEY_ALWAYS_INLINE void write_character(CharType c) { - oa->write_character(c); + if (JSON_HEDLEY_UNLIKELY(used == buffer_size)) + { + flush(); + } + buffer[used++] = c; } // no JSON_HEDLEY_NON_NULL: forwards (null, 0) for empty payloads, exactly as // the type-erased path already did before this sink existed + JSON_HEDLEY_ALWAYS_INLINE void write_characters(const CharType* s, std::size_t length) { - oa->write_characters(s, length); + if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length)) + { + write_large(s, length); + return; + } + if (length != 0) + { + std::memcpy(buffer.data() + used, s, length * sizeof(CharType)); + used += length; + } } /// space for writing up to @a length characters (see /// output_vector_sink::reserve) - CharType* reserve(const std::size_t length) noexcept + JSON_HEDLEY_ALWAYS_INLINE + CharType* reserve(const std::size_t length) { JSON_ASSERT(length <= max_reserve_size); - static_cast(length); - return reserved.data(); + if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length)) + { + flush(); + } + return buffer.data() + used; } - /// write the first @a length characters written to the space from reserve() - void commit(const std::size_t length) + /// keep the first @a length characters written to the space from reserve() + JSON_HEDLEY_ALWAYS_INLINE + void commit(const std::size_t length) noexcept { - oa->write_characters(reserved.data(), length); + used += length; + } + + /// pass everything written so far on to the adapter + void flush() + { + if (used != 0) + { + // reset first: the characters count as written even if the adapter throws + const std::size_t length = used; + used = 0; + oa->write_characters(buffer.data(), length); + } } private: + /// a write that does not fit the space left in the buffer + JSON_HEDLEY_NEVER_INLINE + void write_large(const CharType* s, const std::size_t length) + { + flush(); + if (length >= buffer_size) + { + oa->write_characters(s, length); + } + else + { + std::memcpy(buffer.data(), s, length * sizeof(CharType)); + used = length; + } + } + + /// large enough for reserve() at any time + static constexpr std::size_t buffer_size = 4 * max_reserve_size; + output_adapter_t oa; - std::array reserved{}; + /// number of characters of @ref buffer not yet passed on + std::size_t used = 0; + std::array buffer{}; }; /// @brief whether std::basic_string has a non-deprecated std::char_traits @@ -24792,9 +24884,22 @@ class binary_writer template < typename SinkType = OutputSinkType, typename std::enable_if < std::is_constructible>::value, int >::type = 0 > explicit binary_writer(output_adapter_t adapter, const error_handler_t error_handler_ = binary_writer_default_error_handler()) - : oa(SinkType(std::move(adapter))), error_handler(error_handler_) + : oa(std::move(adapter)), error_handler(error_handler_) {} + /*! + @brief pass on everything written so far to the output adapter + + A writer over an output_adapter_sink buffers what it writes; this has to + be called after the last write, or the buffered end is only passed on, and + an error of the adapter lost, when the writer is destroyed. A writer over + an output_vector_sink writes straight into the vector. + */ + void flush() + { + oa.flush(); + } + /*! @param[in] j JSON value to serialize @throw type_error.316 if a string value or an object key is not valid @@ -38984,7 +39089,9 @@ public: static void to_cbor(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_cbor(j); + binary_writer writer(o, error_handler); + writer.write_cbor(j); + writer.flush(); } /// @brief create a CBOR serialization of a given JSON value @@ -38992,7 +39099,9 @@ public: static void to_cbor(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_cbor(j); + binary_writer writer(o, error_handler); + writer.write_cbor(j); + writer.flush(); } /// @brief create a MessagePack serialization of a given JSON value @@ -39011,7 +39120,9 @@ public: static void to_msgpack(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = error_handler_t::keep) { - binary_writer(o, error_handler).write_msgpack(j); + binary_writer writer(o, error_handler); + writer.write_msgpack(j); + writer.flush(); } /// @brief create a MessagePack serialization of a given JSON value @@ -39019,7 +39130,9 @@ public: static void to_msgpack(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = error_handler_t::keep) { - binary_writer(o, error_handler).write_msgpack(j); + binary_writer writer(o, error_handler); + writer.write_msgpack(j); + writer.flush(); } /// @brief create a UBJSON serialization of a given JSON value @@ -39041,7 +39154,9 @@ public: const bool use_size = false, const bool use_type = false, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type); + writer.flush(); } /// @brief create a UBJSON serialization of a given JSON value @@ -39050,7 +39165,9 @@ public: const bool use_size = false, const bool use_type = false, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type); + writer.flush(); } /// @brief create a BJData serialization of a given JSON value @@ -39074,7 +39191,9 @@ public: const bjdata_version_t version = bjdata_version_t::draft2, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type, true, true, version); + writer.flush(); } /// @brief create a BJData serialization of a given JSON value @@ -39084,7 +39203,9 @@ public: const bjdata_version_t version = bjdata_version_t::draft2, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version); + binary_writer writer(o, error_handler); + writer.write_ubjson(j, use_size, use_type, true, true, version); + writer.flush(); } /// @brief create a BSON serialization of a given JSON value @@ -39103,7 +39224,9 @@ public: static void to_bson(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_bson(j); + binary_writer writer(o, error_handler); + writer.write_bson(j); + writer.flush(); } /// @brief create a BSON serialization of a given JSON value @@ -39111,7 +39234,9 @@ public: static void to_bson(const basic_json& j, detail::output_adapter o, const error_handler_t error_handler = detail::binary_writer_default_error_handler()) { - binary_writer(o, error_handler).write_bson(j); + binary_writer writer(o, error_handler); + writer.write_bson(j); + writer.flush(); } /// @brief create a BON8 serialization of a given JSON value @@ -39128,14 +39253,18 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ static void to_bon8(const basic_json& j, detail::output_adapter o) { - binary_writer(o).write_bon8(j); + binary_writer writer(o); + writer.write_bon8(j); + writer.flush(); } /// @brief create a BON8 serialization of a given JSON value /// @sa https://json.nlohmann.me/api/basic_json/to_bon8/ static void to_bon8(const basic_json& j, detail::output_adapter o) { - binary_writer(o).write_bon8(j); + binary_writer writer(o); + writer.write_bon8(j); + writer.flush(); } /// @brief create a JSON value from an input in CBOR format