From 5d86b761f5f6d8e844c7f63dcb95a98cfc2e4e14 Mon Sep 17 00:00:00 2001 From: Vasily Chekalkin Date: Mon, 2 Mar 2026 09:22:44 +1100 Subject: [PATCH] Packer: bulk write via if constexpr requires store_.write() Optimise four hot emit sites (emplace_integral, emplace_combined, pack_type(string), pack_type(vector)) to call store_.write(ptr, len) in one shot instead of iterating byte-by-byte through std::copy. Falls back to std::copy for iterators without write(), so the change is fully backward-compatible. Add a general CTAD deduction guide for any output_iterator that exposes a byte value_type, complementing the existing back_insert_iterator guide. Co-Authored-By: Claude Sonnet 4.6 --- include/msgpack23/msgpack23.h | 28 ++++++++-- tests/CMakeLists.txt | 1 + tests/bulk_write_tests.cpp | 98 +++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 tests/bulk_write_tests.cpp diff --git a/include/msgpack23/msgpack23.h b/include/msgpack23/msgpack23.h index 0ba7b22..9612451 100644 --- a/include/msgpack23/msgpack23.h +++ b/include/msgpack23/msgpack23.h @@ -458,8 +458,12 @@ namespace msgpack23 { throw std::length_error("String is too long to be serialized."); } - std::copy(reinterpret_cast(value.data()), - reinterpret_cast(value.data() + value.size()), store_); + auto const *src = reinterpret_cast(value.data()); + if constexpr (requires { store_.write(src, value.size()); }) { + store_.write(src, value.size()); + } else { + std::copy(src, src + value.size(), store_); + } } void pack_type(std::vector const &value) { @@ -473,8 +477,12 @@ namespace msgpack23 { } else { throw std::length_error("Vector is too long to be serialized."); } - std::copy(reinterpret_cast(value.data()), - reinterpret_cast(value.data() + value.size()), store_); + auto const *src = reinterpret_cast(value.data()); + if constexpr (requires { store_.write(src, value.size()); }) { + store_.write(src, value.size()); + } else { + std::copy(src, src + value.size(), store_); + } } template @@ -490,7 +498,11 @@ namespace msgpack23 { throw std::length_error("Span is too long to be serialized."); } auto const *src = reinterpret_cast(value.data()); - std::copy(src, src + value.size(), store_); + if constexpr (requires { store_.write(src, value.size()); }) { + store_.write(src, value.size()); + } else { + std::copy(src, src + value.size(), store_); + } } }; @@ -499,6 +511,12 @@ namespace msgpack23 { Packer(std::back_insert_iterator) -> Packer >; + template + requires requires { typename Iter::value_type; } + && byte_type + && std::output_iterator + Packer(Iter) -> Packer; + template concept packable_object = requires(T t, P p) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d4513f9..8c2d535 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,6 +7,7 @@ include(GoogleTest) add_executable( msgpack23_tests array_tests.cpp + bulk_write_tests.cpp byte_type_tests.cpp chrono_tests.cpp exception_tests.cpp diff --git a/tests/bulk_write_tests.cpp b/tests/bulk_write_tests.cpp new file mode 100644 index 0000000..457c8e4 --- /dev/null +++ b/tests/bulk_write_tests.cpp @@ -0,0 +1,98 @@ +// +// Created by Neara Software on 06/03/2026. +// + +#include +#include + +namespace { + + // Counts individual byte emplace calls (operator=) and bulk write() calls. + // test_container holds a raw pointer so that all copies (the Packer stores + // the iterator by value) update the same counts object. + struct counts { + std::size_t emplace_count{0}; + std::size_t write_count{0}; + }; + + struct test_container { + using value_type = std::uint8_t; + using difference_type = std::ptrdiff_t; + + counts *c; + + test_container &operator*() { return *this; } + test_container &operator++() { return *this; } + test_container operator++(int) { return *this; } + + test_container &operator=(std::uint8_t) { + ++c->emplace_count; + return *this; + } + + void write(const std::uint8_t *, std::size_t) { + ++c->write_count; + } + }; + + template + counts pack(T const &value) { + counts c{}; + test_container iter{&c}; + msgpack23::Packer packer{iter}; + packer(value); + return c; + } + + // ── int8_t(42) ──────────────────────────────────────────────────────────── + // 42 > 31: int8 format byte + value byte, no write. + TEST(msgpack23_bulk_write, Int8UsesNoBulkWrite) { + auto c = pack(std::int8_t{42}); + EXPECT_EQ(c.emplace_count, 2u); + EXPECT_EQ(c.write_count, 0u); + } + + // ── uint32_t(70000) ─────────────────────────────────────────────────────── + // 70000 > 0xFFFF: uint32 format byte + 4 bytes via std::copy, no write. + // emplace_integral's requires-check on sizeof(T) does not match write(). + TEST(msgpack23_bulk_write, Uint32UsesNoBulkWrite) { + auto c = pack(std::uint32_t{70'000}); + EXPECT_EQ(c.emplace_count, 5u); + EXPECT_EQ(c.write_count, 0u); + } + + // ── std::string{"hello"} ───────────────────────────────────────────────── + // fixstr header byte (emplace) + body via write(). + TEST(msgpack23_bulk_write, StringUsesBulkWrite) { + auto c = pack(std::string{"hello"}); + EXPECT_EQ(c.emplace_count, 1u); + EXPECT_EQ(c.write_count, 1u); + } + + // ── std::vector ───────────────────────────────────────────────── + // bin8 format byte + length byte (emplace) + body via write(). + TEST(msgpack23_bulk_write, VectorUsesBulkWrite) { + auto c = pack(std::vector{1, 2, 3, 4, 5}); + EXPECT_EQ(c.emplace_count, 2u); + EXPECT_EQ(c.write_count, 1u); + } + + // ── std::span ───────────────────────────────────────────── + // Same bin8 layout as vector. + TEST(msgpack23_bulk_write, SpanUsesBulkWrite) { + std::vector storage{10, 20, 30}; + auto c = pack(std::span{storage}); + EXPECT_EQ(c.emplace_count, 2u); + EXPECT_EQ(c.write_count, 1u); + } + + // ── std::vector{"foo","bar","baz"} ─────────────────────────── + // fixarray header byte + per element: fixstr header byte + write(). + TEST(msgpack23_bulk_write, VectorOfStringsUsesBulkWritePerElement) { + std::vector strings{"foo", "bar", "baz"}; + auto c = pack(strings); + EXPECT_EQ(c.emplace_count, 4u); + EXPECT_EQ(c.write_count, 3u); + } + +} // namespace