Skip to content

Commit 77458df

Browse files
authored
Add bulk I/O missed in the recent std::array optimization (#760)
1 parent b2fc5ac commit 77458df

8 files changed

Lines changed: 734 additions & 0 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#include <benchmark/benchmark.h>
2+
3+
#include <array>
4+
#include <cstddef>
5+
#include <cstdint>
6+
#include <rfl/cereal.hpp>
7+
8+
namespace bulk_numeric_arrays_cereal {
9+
10+
struct Pose {
11+
std::array<double, 4> rotation;
12+
std::array<double, 3> translation;
13+
14+
bool operator==(const Pose&) const = default;
15+
};
16+
17+
template <class T, std::size_t N>
18+
std::array<T, N> make_array() {
19+
std::array<T, N> values;
20+
for (std::size_t i = 0; i < N; ++i) {
21+
values[i] = static_cast<T>((i * 17 + 3) % 251);
22+
}
23+
return values;
24+
}
25+
26+
template <bool Read, class T>
27+
void run(benchmark::State& state, T input) {
28+
const auto bytes = rfl::cereal::write(input);
29+
const auto decoded = rfl::cereal::read<T>(bytes);
30+
if (!decoded || *decoded != input) {
31+
state.SkipWithError("Cereal round trip changed the input");
32+
return;
33+
}
34+
35+
// Time the public byte-container APIs, including stream setup and output
36+
// allocation. Fixture generation and correctness checks are outside the loop.
37+
for (auto _ : state) {
38+
if constexpr (Read) {
39+
auto result = rfl::cereal::read<T>(bytes);
40+
benchmark::DoNotOptimize(result);
41+
} else {
42+
benchmark::DoNotOptimize(input);
43+
auto result = rfl::cereal::write(input);
44+
benchmark::DoNotOptimize(result);
45+
}
46+
benchmark::ClobberMemory();
47+
}
48+
state.SetItemsProcessed(state.iterations());
49+
state.SetBytesProcessed(state.iterations() *
50+
static_cast<std::int64_t>(bytes.size()));
51+
state.counters["serialized_bytes"] = static_cast<double>(bytes.size());
52+
}
53+
54+
template <bool Read, class T, std::size_t N>
55+
void BM_array(benchmark::State& state) {
56+
run<Read>(state, make_array<T, N>());
57+
}
58+
59+
template <bool Read>
60+
void BM_pose(benchmark::State& state) {
61+
run<Read>(state, Pose{{0.0, 0.0, 0.0, 1.0}, {1.25, -2.5, 3.75}});
62+
}
63+
64+
#define RFL_CEREAL_ARRAY_BENCHMARK(T, N) \
65+
BENCHMARK_TEMPLATE(BM_array, true, T, N) \
66+
->Name("bulk_numeric_arrays/cereal/" #T "_" #N "/read"); \
67+
BENCHMARK_TEMPLATE(BM_array, false, T, N) \
68+
->Name("bulk_numeric_arrays/cereal/" #T "_" #N "/write")
69+
70+
RFL_CEREAL_ARRAY_BENCHMARK(double, 2);
71+
RFL_CEREAL_ARRAY_BENCHMARK(double, 3);
72+
RFL_CEREAL_ARRAY_BENCHMARK(double, 4);
73+
RFL_CEREAL_ARRAY_BENCHMARK(double, 9);
74+
RFL_CEREAL_ARRAY_BENCHMARK(double, 16);
75+
RFL_CEREAL_ARRAY_BENCHMARK(double, 36);
76+
RFL_CEREAL_ARRAY_BENCHMARK(float, 128);
77+
RFL_CEREAL_ARRAY_BENCHMARK(std::uint8_t, 32);
78+
79+
#undef RFL_CEREAL_ARRAY_BENCHMARK
80+
81+
BENCHMARK_TEMPLATE(BM_pose, true)->Name("bulk_numeric_arrays/cereal/pose/read");
82+
BENCHMARK_TEMPLATE(BM_pose, false)
83+
->Name("bulk_numeric_arrays/cereal/pose/write");
84+
85+
} // namespace bulk_numeric_arrays_cereal

‎include/rfl/boost_serialization/Parser.hpp‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#ifndef RFL_BOOST_SERIALIZATION_PARSER_HPP_
22
#define RFL_BOOST_SERIALIZATION_PARSER_HPP_
33

4+
#include <array>
45
#include <boost/archive/binary_iarchive.hpp>
56
#include <boost/archive/binary_oarchive.hpp>
67
#include <cstddef>
@@ -22,6 +23,46 @@
2223
namespace rfl {
2324
namespace parsing {
2425

26+
// Keep bool on the generic path to retain Boost's scalar validity assertions.
27+
// Other archive types (including text archives) keep their generic parser too.
28+
template <class T, std::size_t N, class ProcessorsType>
29+
requires(std::is_arithmetic_v<T> && !std::is_same_v<T, bool> &&
30+
!std::is_const_v<T> && !std::is_volatile_v<T>)
31+
struct Parser<boost_serialization::Reader<::boost::archive::binary_iarchive>,
32+
boost_serialization::Writer<::boost::archive::binary_oarchive>,
33+
std::array<T, N>, ProcessorsType>
34+
: ParserArray<
35+
boost_serialization::Reader<::boost::archive::binary_iarchive>,
36+
boost_serialization::Writer<::boost::archive::binary_oarchive>, T, N,
37+
ProcessorsType> {
38+
using R = boost_serialization::Reader<::boost::archive::binary_iarchive>;
39+
using W = boost_serialization::Writer<::boost::archive::binary_oarchive>;
40+
using Type = std::array<T, N>;
41+
42+
static Result<Type> read(const R&,
43+
const typename R::InputVarType& _var) noexcept {
44+
try {
45+
Type values;
46+
if constexpr (N != 0) {
47+
_var.ar->load_binary(values.data(), N * sizeof(T));
48+
}
49+
return values;
50+
} catch (const std::exception& e) {
51+
return error(std::string("Boost binary array read error: ") + e.what());
52+
}
53+
}
54+
55+
template <class ParentType>
56+
static void write(const W& _w, const Type& _values,
57+
const ParentType& _parent) {
58+
auto arr = Parent<W>::add_fixed_array(_w, N, _parent);
59+
if constexpr (N != 0) {
60+
_w.write_array_data(_values.data(), N);
61+
}
62+
_w.end_array(&arr);
63+
}
64+
};
65+
2566
template <class T, class ProcessorsType>
2667
requires(std::is_arithmetic_v<T> && !std::is_same_v<T, bool> &&
2768
!std::is_same_v<T, char>)

‎include/rfl/cereal/Parser.hpp‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#ifndef RFL_CEREAL_PARSER_HPP_
22
#define RFL_CEREAL_PARSER_HPP_
33

4+
#include <array>
45
#include <cstddef>
56
#include <exception>
67
#include <string>
@@ -15,6 +16,43 @@
1516
namespace rfl {
1617
namespace parsing {
1718

19+
template <class T, std::size_t N, class ProcessorsType>
20+
requires(std::is_arithmetic_v<T> && !std::is_const_v<T> &&
21+
!std::is_volatile_v<T>)
22+
struct Parser<cereal::Reader, cereal::Writer, std::array<T, N>, ProcessorsType>
23+
: ParserArray<cereal::Reader, cereal::Writer, T, N, ProcessorsType> {
24+
using R = cereal::Reader;
25+
using W = cereal::Writer;
26+
using Type = std::array<T, N>;
27+
28+
static Result<Type> read(const R&,
29+
const typename R::InputVarType& _var) noexcept {
30+
try {
31+
Type values;
32+
if constexpr (N != 0) {
33+
// Keep the typed pointer: portable binary swaps each T separately.
34+
(*_var.archive_)(::cereal::binary_data(values.data(), N * sizeof(T)));
35+
}
36+
return values;
37+
} catch (const std::exception& e) {
38+
return error(std::string("Cereal array read error: ") + e.what());
39+
}
40+
}
41+
42+
template <class ParentType>
43+
static void write(const W& _w, const Type& _values,
44+
const ParentType& _parent) {
45+
// Even empty arrays need parent metadata, but never a length prefix.
46+
auto arr = Parent<W>::add_fixed_array(_w, N, _parent);
47+
if constexpr (N != 0) {
48+
// Cereal encodes arithmetic scalars (including bool and character types)
49+
// as sizeof(T) bytes, just like this contiguous std::array payload.
50+
_w.write_array_data(_values.data(), N);
51+
}
52+
_w.end_array(&arr);
53+
}
54+
};
55+
1856
template <class T, class ProcessorsType>
1957
requires(std::is_arithmetic_v<T> && !std::is_same_v<T, bool> &&
2058
!std::is_same_v<T, char>)

‎include/rfl/yas/Parser.hpp‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#ifndef RFL_YAS_PARSER_HPP_
22
#define RFL_YAS_PARSER_HPP_
33

4+
#include <array>
45
#include <cstddef>
56
#include <exception>
67
#include <string>
@@ -19,6 +20,42 @@
1920
namespace rfl {
2021
namespace parsing {
2122

23+
template <class T, std::size_t N, class ProcessorsType>
24+
requires((std::is_integral_v<T> || std::is_same_v<T, float> ||
25+
std::is_same_v<T, double>) &&
26+
!std::is_const_v<T> && !std::is_volatile_v<T>)
27+
struct Parser<yas::Reader, yas::Writer, std::array<T, N>, ProcessorsType>
28+
: ParserArray<yas::Reader, yas::Writer, T, N, ProcessorsType> {
29+
using R = yas::Reader;
30+
using W = yas::Writer;
31+
using Type = std::array<T, N>;
32+
33+
static Result<Type> read(const R&,
34+
const typename R::InputVarType& _var) noexcept {
35+
try {
36+
Type values;
37+
if constexpr (N != 0) {
38+
// This adapter uses YAS's non-compacted, host-endian binary archive.
39+
_var.ar->read(values.data(), N * sizeof(T));
40+
}
41+
return values;
42+
} catch (const std::exception& e) {
43+
return error(std::string("yas array read error: ") + e.what());
44+
}
45+
}
46+
47+
template <class ParentType>
48+
static void write(const W& _w, const Type& _values,
49+
const ParentType& _parent) {
50+
auto arr = Parent<W>::add_fixed_array(_w, N, _parent);
51+
if constexpr (N != 0) {
52+
// bool and character scalars also use their native representation.
53+
arr.ar->write(_values.data(), N * sizeof(T));
54+
}
55+
_w.end_array(&arr);
56+
}
57+
};
58+
2259
template <class T, class ProcessorsType>
2360
requires((std::is_integral_v<T> || std::is_same_v<T, float> ||
2461
std::is_same_v<T, double>) &&
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#include <boost/archive/text_iarchive.hpp>
2+
#include <boost/archive/text_oarchive.hpp>
3+
#include <rfl/NoFieldNames.hpp>
4+
#include <rfl/boost_serialization.hpp>
5+
#include <rfl/parsing/ParserArray.hpp>
6+
#include <sstream>
7+
8+
#include "../check_numeric_array_io.hpp"
9+
10+
namespace rfl::parsing {
11+
template <class T, std::size_t N, class... Ps>
12+
struct Parser<boost_serialization::Reader<::boost::archive::binary_iarchive>,
13+
boost_serialization::Writer<::boost::archive::binary_oarchive>,
14+
std::array<T, N>,
15+
Processors<test_numeric_array_io::GenericArrays, Ps...>>
16+
: test_numeric_array_io::GenericArrayParser<
17+
boost_serialization::Reader<::boost::archive::binary_iarchive>,
18+
boost_serialization::Writer<::boost::archive::binary_oarchive>, T, N,
19+
Processors<test_numeric_array_io::GenericArrays, Ps...>> {};
20+
} // namespace rfl::parsing
21+
22+
namespace test_numeric_array_io {
23+
namespace {
24+
template <class... Ps>
25+
constexpr auto write = [](const auto& input) {
26+
return rfl::boost_serialization::write<Ps...>(input);
27+
};
28+
29+
template <class... Ps>
30+
constexpr auto read = []<class T>(const auto& bytes) {
31+
return rfl::boost_serialization::read<T, Ps...>(bytes);
32+
};
33+
34+
template <class... Ps>
35+
void check(const auto& input) {
36+
check_compatible(input, write<Ps...>, read<Ps...>,
37+
write<GenericArrays, Ps...>, read<GenericArrays, Ps...>);
38+
}
39+
} // namespace
40+
41+
TEST(boost_serialization, numeric_array_wire_compatibility) {
42+
// bool deliberately retains the production generic parser, whose N == 1
43+
// raw buffer triggers GCC 13's -Wmaybe-uninitialized false positive. Keep
44+
// bool coverage at N == 0, 4, and 65; all optimized types also test N == 1.
45+
check_types<false>(write<>, read<>, write<GenericArrays>,
46+
read<GenericArrays>);
47+
}
48+
49+
TEST(boost_serialization, numeric_array_parents) {
50+
check_parents([](const auto& input) { check(input); });
51+
check_parents([](const auto& input) { check<rfl::NoFieldNames>(input); });
52+
}
53+
54+
TEST(boost_serialization, numeric_array_non_arithmetic_fallback) {
55+
check_non_arithmetic([](const auto& input) { check(input); });
56+
}
57+
58+
TEST(boost_serialization, numeric_array_truncated_input) {
59+
check_truncated(write<>, read<>);
60+
check_truncated(write<>, read<GenericArrays>);
61+
}
62+
63+
TEST(boost_serialization, numeric_array_text_archive_fallback) {
64+
using I = boost::archive::text_iarchive;
65+
using O = boost::archive::text_oarchive;
66+
using R = rfl::boost_serialization::Reader<I>;
67+
using W = rfl::boost_serialization::Writer<O>;
68+
const std::array<double, 3> input{1.25, -2.5, 0.0};
69+
std::ostringstream output;
70+
{
71+
O archive(output);
72+
rfl::boost_serialization::write<I>(archive, input);
73+
}
74+
std::ostringstream generic;
75+
{
76+
O archive(generic);
77+
const W writer(&archive);
78+
rfl::parsing::ParserArray<R, W, double, 3, rfl::Processors<>>::write(
79+
writer, input, rfl::parsing::Parent<W>::Root{});
80+
}
81+
EXPECT_EQ(output.str(), generic.str());
82+
std::istringstream stream(output.str());
83+
I archive(stream);
84+
const auto decoded =
85+
rfl::boost_serialization::read_from_archive<std::array<double, 3>, I, O>(
86+
archive);
87+
ASSERT_TRUE(decoded) << decoded.error().what();
88+
EXPECT_EQ(*decoded, input);
89+
}
90+
91+
TEST(boost_serialization, numeric_array_binary_archive_options) {
92+
using I = boost::archive::binary_iarchive;
93+
using O = boost::archive::binary_oarchive;
94+
const std::array<long double, 3> input{1.25L, -2.5L, 3.75L};
95+
for (const auto flags : {0u, unsigned(boost::archive::no_header |
96+
boost::archive::no_tracking)}) {
97+
const auto serialize = [&]<class... Ps>(rfl::Processors<Ps...>) {
98+
std::ostringstream stream;
99+
{
100+
O archive(stream, flags);
101+
rfl::boost_serialization::write<I, O, Ps...>(archive, input);
102+
}
103+
return stream.str();
104+
};
105+
const auto bytes = serialize(rfl::Processors<>{});
106+
EXPECT_EQ(bytes, serialize(rfl::Processors<GenericArrays>{}));
107+
std::istringstream stream(bytes);
108+
I archive(stream, flags);
109+
const auto decoded =
110+
rfl::boost_serialization::read_from_archive<std::array<long double, 3>,
111+
I, O>(archive);
112+
ASSERT_TRUE(decoded) << decoded.error().what();
113+
EXPECT_EQ(*decoded, input);
114+
}
115+
}
116+
117+
} // namespace test_numeric_array_io

0 commit comments

Comments
 (0)