From 994e375e1cf4494c9e07de3f93c2af6d9202bfe6 Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Thu, 15 Oct 2026 02:45:04 +0300 Subject: [PATCH] Optimize numeric vectors in Cereal, YAS, and Boost binary Preserve each adapter's array length and parent metadata while transferring numeric vector payloads in bulk. Keep Cereal portable byte swapping and restrict Boost optimization to standard binary archives. Add wire-compatibility and error-path regression tests, plus deterministic numeric-vector and World benchmarks for all three formats. Validated 141 format tests, four focused Cereal size-type tests, and 180 benchmark case runs. --- .github/workflows/linux.yaml | 1 + .github/workflows/macos.yaml | 1 + .github/workflows/windows.yaml | 1 + benchmarks/all/bulk_numeric_vectors.cpp | 244 ++++++++++++++++++ include/rfl/boost_serialization/Parser.hpp | 52 ++++ include/rfl/boost_serialization/Writer.hpp | 8 + include/rfl/cereal/Parser.hpp | 42 +++ include/rfl/cereal/Writer.hpp | 10 + include/rfl/yas/Parser.hpp | 43 +++ .../test_numeric_vector_io.cpp | 111 ++++++++ tests/cereal/test_numeric_vector_io.cpp | 116 +++++++++ tests/check_numeric_vector_io.hpp | 134 ++++++++++ tests/yas/test_numeric_vector_io.cpp | 67 +++++ 13 files changed, 830 insertions(+) create mode 100644 benchmarks/all/bulk_numeric_vectors.cpp create mode 100644 tests/boost_serialization/test_numeric_vector_io.cpp create mode 100644 tests/cereal/test_numeric_vector_io.cpp create mode 100644 tests/check_numeric_vector_io.hpp create mode 100644 tests/yas/test_numeric_vector_io.cpp diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index 1b11ed0ec..60a2f3ec9 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -126,6 +126,7 @@ jobs: ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=points_read >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=bulk_numeric_vectors >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo '## Benchmarks for JSON' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 40ab577d1..220709906 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -68,6 +68,7 @@ jobs: ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=points_read >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=bulk_numeric_vectors >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo '## Benchmarks for JSON' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 14ca7b2fc..40fe81d71 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -67,6 +67,7 @@ jobs: ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=points_read >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=bulk_numeric_vectors >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo '## Benchmarks for JSON' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/benchmarks/all/bulk_numeric_vectors.cpp b/benchmarks/all/bulk_numeric_vectors.cpp new file mode 100644 index 000000000..3819ff0f3 --- /dev/null +++ b/benchmarks/all/bulk_numeric_vectors.cpp @@ -0,0 +1,244 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bulk_numeric_vectors { + +struct Point2 { + double x; + double y; + + bool operator==(const Point2&) const = default; +}; + +struct Camera { + std::vector keypoints; + std::vector landmark_ids; + + bool operator==(const Camera&) const = default; +}; + +struct World { + std::vector cameras; + + bool operator==(const World&) const = default; +}; + +static Point2 make_point(std::size_t index) { + return { + static_cast(static_cast(index % 65536) - 32768) * + 0.125, + static_cast(static_cast((index * 17) % 65536) - + 32768) * + 0.25}; +} + +static std::uint64_t make_id(std::size_t index) { + return static_cast(index) * UINT64_C(0x9e3779b97f4a7c15) + + UINT64_C(0x123456789abcdef0); +} + +static std::vector make_ids(std::size_t count) { + std::vector values(count); + for (std::size_t i = 0; i < count; ++i) values[i] = make_id(i); + return values; +} + +static std::vector make_points(std::size_t count) { + std::vector values(count); + for (std::size_t i = 0; i < count; ++i) values[i] = make_point(i); + return values; +} + +static World make_world(std::size_t cameras, std::size_t points_per_camera) { + World world; + world.cameras.resize(cameras); + for (std::size_t i = 0; i < cameras; ++i) { + auto& camera = world.cameras[i]; + camera.keypoints.resize(points_per_camera); + camera.landmark_ids.resize(points_per_camera); + for (std::size_t j = 0; j < points_per_camera; ++j) { + const auto index = i * points_per_camera + j; + camera.keypoints[j] = make_point(index); + camera.landmark_ids[j] = make_id(index); + } + } + return world; +} + +enum class Format { cereal, yas, boost_serialization }; + +template +void write(const T& input, std::ostream& stream) { + if constexpr (F == Format::cereal) + rfl::cereal::write(input, stream); + else if constexpr (F == Format::yas) + rfl::yas::write(input, stream); + else + rfl::boost_serialization::write(input, stream); +} + +template +auto read(const std::string& bytes) { + if constexpr (F == Format::yas) { + return rfl::yas::read(bytes); + } else { + std::istringstream stream(bytes); + if constexpr (F == Format::cereal) + return rfl::cereal::read(stream); + else + return rfl::boost_serialization::read(stream); + } +} + +template +static void run(benchmark::State& state, const T& input, std::int64_t items) { + std::ostringstream output; + write(input, output); + const auto bytes = output.str(); + { + const auto result = read(bytes); + if (!result) { + state.SkipWithError(result.error().what()); + return; + } + if (*result != input) { + state.SkipWithError("Binary round trip changed the input"); + return; + } + } + + // Stream construction, serialization, and destruction are timed. Fixture + // generation, output extraction, and equality checks are outside the loop. + for (auto _ : state) { + if constexpr (Read) { + auto result = read(bytes); + if (!result) { + state.SkipWithError(result.error().what()); + break; + } + benchmark::DoNotOptimize(result); + } else { + std::ostringstream stream; + write(input, stream); + benchmark::DoNotOptimize(stream); + } + } + state.SetItemsProcessed(state.iterations() * items); + state.SetBytesProcessed(state.iterations() * + static_cast(bytes.size())); + state.counters["serialized_bytes"] = static_cast(bytes.size()); +} + +template +static void BM_bulk_numeric_vectors_ids(benchmark::State& state) { + const auto input = make_ids(static_cast(state.range(0))); + run(state, input, state.range(0)); +} + +template +static void BM_bulk_numeric_vectors_points(benchmark::State& state) { + const auto input = make_points(static_cast(state.range(0))); + run(state, input, state.range(0)); +} + +template +static void BM_bulk_numeric_vectors_complete(benchmark::State& state) { + const auto input = make_world(static_cast(state.range(0)), + static_cast(state.range(1))); + run(state, input, state.range(0) * state.range(1)); +} + +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, false, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/ids/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, true, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/ids/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, false, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/points/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, true, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/points/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, false, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/complete/write") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, true, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/complete/read") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, false, Format::yas) + ->Name("bulk_numeric_vectors/yas/ids/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, true, Format::yas) + ->Name("bulk_numeric_vectors/yas/ids/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, false, Format::yas) + ->Name("bulk_numeric_vectors/yas/points/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, true, Format::yas) + ->Name("bulk_numeric_vectors/yas/points/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, false, Format::yas) + ->Name("bulk_numeric_vectors/yas/complete/write") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, true, Format::yas) + ->Name("bulk_numeric_vectors/yas/complete/read") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, false, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/ids/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, true, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/ids/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, false, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/points/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, true, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/points/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, false, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/complete/write") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, true, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/complete/read") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); + +} // namespace bulk_numeric_vectors diff --git a/include/rfl/boost_serialization/Parser.hpp b/include/rfl/boost_serialization/Parser.hpp index 7cf7e6af7..ddc9ec079 100644 --- a/include/rfl/boost_serialization/Parser.hpp +++ b/include/rfl/boost_serialization/Parser.hpp @@ -1,6 +1,15 @@ #ifndef RFL_BOOST_SERIALIZATION_PARSER_HPP_ #define RFL_BOOST_SERIALIZATION_PARSER_HPP_ +#include +#include +#include +#include +#include +#include +#include +#include + #include "../Generic.hpp" #include "../NamedTuple.hpp" #include "../Tuple.hpp" @@ -13,6 +22,49 @@ namespace rfl { namespace parsing { +template + requires(std::is_arithmetic_v && !std::is_same_v && + !std::is_same_v) +struct Parser, + boost_serialization::Writer<::boost::archive::binary_oarchive>, + std::vector, ProcessorsType> + : VectorParser< + boost_serialization::Reader<::boost::archive::binary_iarchive>, + boost_serialization::Writer<::boost::archive::binary_oarchive>, + std::vector, ProcessorsType> { + using R = boost_serialization::Reader<::boost::archive::binary_iarchive>; + using W = boost_serialization::Writer<::boost::archive::binary_oarchive>; + using Type = std::vector; + + static Result read(const R&, + const typename R::InputVarType& _var) noexcept { + try { + std::uint64_t size; + *_var.ar >> size; + Type values; + if (size > values.max_size()) { + return error("Array length exceeds vector max_size"); + } + values.resize(static_cast(size)); + if (!values.empty()) { + _var.ar->load_binary(values.data(), values.size() * sizeof(T)); + } + return values; + } catch (const std::exception& e) { + return error(std::string("Boost binary array read error: ") + e.what()); + } + } + + template + static void write(const W& _w, const Type& _values, + const ParentType& _parent) { + // Keep the adapter's array prefix and parent metadata unchanged. + auto arr = Parent::add_array(_w, _values.size(), _parent); + _w.write_array_data(_values.data(), _values.size()); + _w.end_array(&arr); + } +}; + /// Boost.Serialization requires us to explicitly set all fields. Because /// of that, we require all of the fields and then set them to nullptr, if /// necessary. diff --git a/include/rfl/boost_serialization/Writer.hpp b/include/rfl/boost_serialization/Writer.hpp index ccf1e74f9..8c18842d5 100644 --- a/include/rfl/boost_serialization/Writer.hpp +++ b/include/rfl/boost_serialization/Writer.hpp @@ -1,6 +1,7 @@ #ifndef RFL_BOOST_SERIALIZATION_WRITER_HPP_ #define RFL_BOOST_SERIALIZATION_WRITER_HPP_ +#include #include #include #include @@ -254,6 +255,13 @@ class Writer { /// @param _obj Pointer to the object (unused) void end_object(OutputObjectType* /*_obj*/) const {} + template + void write_array_data(const T* _data, const std::size_t _size) const { + if (_size != 0) { + ar_->save_binary(_data, _size * sizeof(T)); + } + } + private: void write_size(const size_t _size) const { *ar_ << static_cast(_size); diff --git a/include/rfl/cereal/Parser.hpp b/include/rfl/cereal/Parser.hpp index 626b0138e..616f2f6f6 100644 --- a/include/rfl/cereal/Parser.hpp +++ b/include/rfl/cereal/Parser.hpp @@ -1,6 +1,12 @@ #ifndef RFL_CEREAL_PARSER_HPP_ #define RFL_CEREAL_PARSER_HPP_ +#include +#include +#include +#include +#include + #include "../internal/no_field_names_v.hpp" #include "../parsing/Parser.hpp" #include "Reader.hpp" @@ -9,6 +15,42 @@ namespace rfl { namespace parsing { +template + requires(std::is_arithmetic_v && !std::is_same_v && + !std::is_same_v) +struct Parser, ProcessorsType> + : VectorParser, + ProcessorsType> { + using R = cereal::Reader; + using W = cereal::Writer; + using Type = std::vector; + + static Result read(const R&, + const typename R::InputVarType& _var) noexcept { + try { + std::size_t size; + (*_var.archive_)(::cereal::make_size_tag(size)); + Type values(size); + if (!values.empty()) { + (*_var.archive_)( + ::cereal::binary_data(values.data(), values.size() * sizeof(T))); + } + return values; + } catch (const std::exception& e) { + return error(std::string("Cereal array read error: ") + e.what()); + } + } + + template + static void write(const W& _w, const Type& _values, + const ParentType& _parent) { + // Keep the adapter's array prefix and parent metadata unchanged. + auto arr = Parent::add_array(_w, _values.size(), _parent); + _w.write_array_data(_values.data(), _values.size()); + _w.end_array(&arr); + } +}; + /// Cereal requires us to explicitly set the number of fields in advance. /// Because of that, we require all of the fields and then set them to nullptr, /// if necessary. diff --git a/include/rfl/cereal/Writer.hpp b/include/rfl/cereal/Writer.hpp index ec20478d5..15a2993fe 100644 --- a/include/rfl/cereal/Writer.hpp +++ b/include/rfl/cereal/Writer.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -177,6 +178,15 @@ class Writer { void end_object(OutputObjectType* _obj) const noexcept; + template + void write_array_data(const T* _data, const std::size_t _size) const { + if (_size != 0) { + // A pointer value preserves the element width in Cereal BinaryData. + (*archive_)(::cereal::binary_data(static_cast(_data), + _size * sizeof(T))); + } + } + private: void add_string_view(const std::string_view& _str) const; diff --git a/include/rfl/yas/Parser.hpp b/include/rfl/yas/Parser.hpp index ce5e4c833..d7ede37c5 100644 --- a/include/rfl/yas/Parser.hpp +++ b/include/rfl/yas/Parser.hpp @@ -1,6 +1,12 @@ #ifndef RFL_YAS_PARSER_HPP_ #define RFL_YAS_PARSER_HPP_ +#include +#include +#include +#include +#include + #include "../Generic.hpp" #include "../NamedTuple.hpp" #include "../Tuple.hpp" @@ -13,6 +19,43 @@ namespace rfl { namespace parsing { +template + requires((std::is_integral_v || std::is_same_v || + std::is_same_v) && + !std::is_same_v && !std::is_same_v) +struct Parser, ProcessorsType> + : VectorParser, ProcessorsType> { + using R = yas::Reader; + using W = yas::Writer; + using Type = std::vector; + + static Result read(const R&, + const typename R::InputVarType& _var) noexcept { + try { + std::size_t size; + (*_var.ar) & size; + Type values(size); + if (!values.empty()) { + _var.ar->read(values.data(), values.size() * sizeof(T)); + } + return values; + } catch (const std::exception& e) { + return error(std::string("yas array read error: ") + e.what()); + } + } + + template + static void write(const W& _w, const Type& _values, + const ParentType& _parent) { + // Keep the adapter's array prefix and parent metadata unchanged. + auto arr = Parent::add_array(_w, _values.size(), _parent); + if (!_values.empty()) { + arr.ar->write(_values.data(), _values.size() * sizeof(T)); + } + _w.end_array(&arr); + } +}; + /// Yas requires us to explicitly set the number of fields in advance. /// Because of that, we require all of the fields and then set them to nullptr, /// if necessary. diff --git a/tests/boost_serialization/test_numeric_vector_io.cpp b/tests/boost_serialization/test_numeric_vector_io.cpp new file mode 100644 index 000000000..53023b9a1 --- /dev/null +++ b/tests/boost_serialization/test_numeric_vector_io.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../check_numeric_vector_io.hpp" + +namespace test_numeric_vector_io { +namespace { +using R = rfl::boost_serialization::Reader; +using W = rfl::boost_serialization::Writer; +using Ps = rfl::Processors<>; + +template +auto legacy_write(const std::vector& input) { + std::ostringstream stream; + boost::archive::binary_oarchive archive( + stream, boost::archive::no_header | boost::archive::no_tracking); + W writer(&archive); + rfl::parsing::VectorParser, Ps>::write( + writer, input, typename rfl::parsing::Parent::Root{}); + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); +} + +template +auto legacy_read(const std::vector& bytes) { + std::istringstream stream(std::string(bytes.begin(), bytes.end())); + boost::archive::binary_iarchive archive( + stream, boost::archive::no_header | boost::archive::no_tracking); + return rfl::parsing::VectorParser, Ps>::read( + R{}, typename R::InputVarType{&archive}); +} +} // namespace + +TEST(boost_serialization, numeric_vector_wire_compatibility) { + check_types( + [](const auto& input) { return rfl::boost_serialization::write(input); }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }, + [](const auto& input) { return legacy_write(input); }, + [](const auto& bytes) { return legacy_read(bytes); }); +} + +TEST(boost_serialization, numeric_vector_parent_metadata) { + check_nested( + [](const auto& input) { return rfl::boost_serialization::write(input); }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }); + check_nested( + [](const auto& input) { + return rfl::boost_serialization::write(input); + }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }); +} + +TEST(boost_serialization, numeric_vector_excessive_length) { + const auto make_bytes = [] { + std::ostringstream stream; + boost::archive::binary_oarchive archive( + stream, boost::archive::no_header | boost::archive::no_tracking); + const auto size = std::numeric_limits::max(); + archive << size; + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); + }; + EXPECT_FALSE( + rfl::boost_serialization::read>(make_bytes())); +} + +} // namespace test_numeric_vector_io + +namespace test_numeric_vector_io { + +TEST(boost_serialization, numeric_vector_text_archive_fallback) { + using I = boost::archive::text_iarchive; + using O = boost::archive::text_oarchive; + using TextReader = rfl::boost_serialization::Reader; + using TextWriter = rfl::boost_serialization::Writer; + const std::vector input{1.25, -2.5, 0.0}; + std::ostringstream output; + { + O archive(output); + rfl::boost_serialization::write(archive, input); + } + std::ostringstream legacy; + { + O archive(legacy); + const TextWriter writer(&archive); + rfl::parsing:: + VectorParser, Ps>::write( + writer, input, typename rfl::parsing::Parent::Root{}); + } + EXPECT_EQ(output.str(), legacy.str()); + std::istringstream stream(output.str()); + I archive(stream); + const auto decoded = + rfl::boost_serialization::read_from_archive, I, O>( + archive); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); +} + +} // namespace test_numeric_vector_io diff --git a/tests/cereal/test_numeric_vector_io.cpp b/tests/cereal/test_numeric_vector_io.cpp new file mode 100644 index 000000000..25a67f36f --- /dev/null +++ b/tests/cereal/test_numeric_vector_io.cpp @@ -0,0 +1,116 @@ +#include +#include +#include +#include + +#include "../check_numeric_vector_io.hpp" + +namespace test_numeric_vector_io { +namespace { +using R = rfl::cereal::Reader; +using W = rfl::cereal::Writer; +using Ps = rfl::Processors<>; + +template +auto legacy_write(const std::vector& input) { + std::ostringstream stream; + ::cereal::PortableBinaryOutputArchive archive(stream); + W writer(&archive); + rfl::parsing::VectorParser, Ps>::write( + writer, input, typename rfl::parsing::Parent::Root{}); + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); +} + +template +auto legacy_read(const std::vector& bytes) { + std::istringstream stream(std::string(bytes.begin(), bytes.end())); + ::cereal::PortableBinaryInputArchive archive(stream); + return rfl::parsing::VectorParser, Ps>::read( + R{}, typename R::InputVarType{&archive}); +} +} // namespace + +TEST(cereal, numeric_vector_wire_compatibility) { + check_types( + [](const auto& input) { return rfl::cereal::write(input); }, + [](const auto& bytes) { return rfl::cereal::read(bytes); }, + [](const auto& input) { return legacy_write(input); }, + [](const auto& bytes) { return legacy_read(bytes); }); +} + +TEST(cereal, numeric_vector_parent_metadata) { + check_nested( + [](const auto& input) { return rfl::cereal::write(input); }, + [](const auto& bytes) { return rfl::cereal::read(bytes); }); + check_nested( + [](const auto& input) { + return rfl::cereal::write(input); + }, + [](const auto& bytes) { + return rfl::cereal::read(bytes); + }); +} + +TEST(cereal, numeric_vector_excessive_length) { + const auto make_bytes = [] { + std::ostringstream stream; + ::cereal::PortableBinaryOutputArchive archive(stream); + const auto size = std::numeric_limits::max(); + archive(::cereal::make_size_tag(size)); + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); + }; + EXPECT_FALSE(rfl::cereal::read>(make_bytes())); +} + +} // namespace test_numeric_vector_io + +namespace test_numeric_vector_io { + +TEST(cereal, numeric_vector_endianness) { + using Archive = ::cereal::PortableBinaryOutputArchive; + for (const auto options : + {Archive::Options::LittleEndian(), Archive::Options::BigEndian()}) { + const auto write = [options](const auto& input) { + std::ostringstream stream; + Archive archive(stream, options); + rfl::cereal::write(input, archive); + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); + }; + const auto old_write = [options](const std::vector& input) { + std::ostringstream stream; + Archive archive(stream, options); + W writer(&archive); + rfl::parsing::VectorParser, Ps>::write( + writer, input, typename rfl::parsing::Parent::Root{}); + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); + }; + check_types( + write, + [](const auto& bytes) { return rfl::cereal::read(bytes); }, + old_write, + [](const auto& bytes) { return legacy_read(bytes); }); + } +} + +} // namespace test_numeric_vector_io + +namespace test_numeric_vector_io { +TEST(cereal, numeric_vector_nested_arrays) { + const std::vector> input{{}, {1.25, -2.5}, {3.0}}; + const std::vector> legacy{{}, {1.25, -2.5}, {3.0}}; + const auto bytes = rfl::cereal::write(input); + EXPECT_EQ(bytes, rfl::cereal::write(legacy)); + const auto decoded = rfl::cereal::read>>( + rfl::cereal::write(legacy)); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); + const auto old_decoded = + rfl::cereal::read>>(bytes); + ASSERT_TRUE(old_decoded) << old_decoded.error().what(); + EXPECT_EQ(*old_decoded, legacy); +} +} // namespace test_numeric_vector_io diff --git a/tests/check_numeric_vector_io.hpp b/tests/check_numeric_vector_io.hpp new file mode 100644 index 000000000..abe3e55be --- /dev/null +++ b/tests/check_numeric_vector_io.hpp @@ -0,0 +1,134 @@ +#ifndef CHECK_NUMERIC_VECTOR_IO_HPP_ +#define CHECK_NUMERIC_VECTOR_IO_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace test_numeric_vector_io { + +template +void check_type(Write write, Read read, LegacyWrite legacy_write, + LegacyRead legacy_read) { + const std::vector edge_values = {std::numeric_limits::lowest(), T(0), + T(1), std::numeric_limits::max()}; + for (const auto count : {0, 1, 65}) { + SCOPED_TRACE(count); + std::vector input; + for (int i = 0; i < count; ++i) { + input.push_back(edge_values[i % edge_values.size()]); + } + const auto old_bytes = legacy_write(input); + const auto bytes = write(input); + EXPECT_EQ(bytes, old_bytes); + const auto decoded = read.template operator()>(old_bytes); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); + const auto old_decoded = legacy_read.template operator()(bytes); + ASSERT_TRUE(old_decoded) << old_decoded.error().what(); + EXPECT_EQ(*old_decoded, input); + if (count != 0) { + auto truncated = bytes; + truncated.pop_back(); + EXPECT_FALSE(read.template operator()>(truncated)); + } + } + if constexpr (std::is_floating_point_v) { + const std::vector input = {-T(0), T(1.25), -T(2.5), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + EXPECT_EQ(write(input), legacy_write(input)); + const auto decoded = + read.template operator()>(legacy_write(input)); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); + EXPECT_EQ(write(*decoded), legacy_write(input)); + const std::vector nan_input{std::numeric_limits::quiet_NaN()}; + const auto nan_bytes = legacy_write(nan_input); + EXPECT_EQ(write(nan_input), nan_bytes); + const auto nan_decoded = + read.template operator()>(nan_bytes); + ASSERT_TRUE(nan_decoded) << nan_decoded.error().what(); + ASSERT_EQ(nan_decoded->size(), 1u); + EXPECT_TRUE(std::isnan(nan_decoded->front())); + EXPECT_EQ(write(*nan_decoded), nan_bytes); + } +} + +template +void check_types(Write write, Read read, LegacyWrite legacy_write, + LegacyRead legacy_read) { + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); +} + +template +using Vector = std::vector; +template +using Deque = std::deque; + +template