From dd78450d6e608c7572599aa19469c9766296305b Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Wed, 14 Oct 2026 11:51:50 +0300 Subject: [PATCH 1/2] Avoid eager error allocation when parsing named tuples --- .github/workflows/linux.yaml | 1 + .github/workflows/macos.yaml | 1 + .github/workflows/windows.yaml | 1 + benchmarks/all/points_read.cpp | 144 +++++++++++++++++++++++ include/rfl/parsing/NamedTupleParser.hpp | 2 - 5 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 benchmarks/all/points_read.cpp diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index 5bff25efe..1b11ed0ec 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -125,6 +125,7 @@ jobs: ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY ./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 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 b94c21050..40ab577d1 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -67,6 +67,7 @@ jobs: ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY ./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 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 0f8dba057..14ca7b2fc 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -66,6 +66,7 @@ jobs: ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY ./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 echo '```' >> $GITHUB_STEP_SUMMARY echo '## Benchmarks for JSON' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/benchmarks/all/points_read.cpp b/benchmarks/all/points_read.cpp new file mode 100644 index 000000000..2a80ac9ae --- /dev/null +++ b/benchmarks/all/points_read.cpp @@ -0,0 +1,144 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace points_read { + +struct Point2d { + float x; + float y; + + bool operator==(const Point2d&) const = default; +}; + +using Points = std::vector; + +static Points make_points(std::size_t count) { + Points points; + points.reserve(count); + for (std::size_t i = 0; i < count; ++i) { + // Exactly representable coordinates of both signs, varying across points. + const auto x = static_cast(i % 65536) - 32768; + const auto y = static_cast((i * 17) % 65536) - 32768; + points.push_back( + {static_cast(x) * 0.125f, static_cast(y) * 0.0625f}); + } + return points; +} + +template +static void run(benchmark::State& state, Write write, Read read) { + // Many small structs expose per-object parser overhead, including allocating + // error storage on successful reads. Setup and validation are not timed. + const auto points = make_points(static_cast(state.range(0))); + const auto data = write(points); + { + const auto result = read(data); + if (!result) { + state.SkipWithError(result.error().what()); + return; + } + if (*result != points) { + state.SkipWithError("Point coordinates changed during round trip"); + return; + } + } + + for (auto _ : state) { + auto result = read(data); + if (!result) { + state.SkipWithError(result.error().what()); + break; + } + benchmark::DoNotOptimize(result); + } + + state.SetItemsProcessed(state.iterations() * state.range(0)); + state.SetBytesProcessed(state.iterations() * + static_cast(data.size())); + state.counters["serialized_bytes"] = static_cast(data.size()); +} + +// DefaultIfMissing exercises the parser's default-value path. NoFieldNames +// separates field-name lookup overhead from positional struct parsing. +template +static void BM_points_read_reflect_cpp_cereal(benchmark::State& state) { + run( + state, + [](const Points& points) { return rfl::cereal::write(points); }, + [](const auto& data) { return rfl::cereal::read(data); }); +} +BENCHMARK_TEMPLATE(BM_points_read_reflect_cpp_cereal, rfl::Processors<>) + ->Arg(1000) + ->Arg(1000000) + ->Unit(benchmark::kMillisecond); + +template +static void BM_points_read_reflect_cpp_json(benchmark::State& state) { + run( + state, + [](const Points& points) { return rfl::json::write(points); }, + [](const auto& data) { return rfl::json::read(data); }); +} +BENCHMARK_TEMPLATE(BM_points_read_reflect_cpp_json, rfl::Processors<>) + ->Arg(1000) + ->Arg(1000000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_points_read_reflect_cpp_json, rfl::DefaultIfMissing) + ->Arg(1000) + ->Arg(1000000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_points_read_reflect_cpp_json, rfl::NoFieldNames) + ->Arg(1000) + ->Arg(1000000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_points_read_reflect_cpp_json, rfl::NoFieldNames, + rfl::DefaultIfMissing) + ->Arg(1000) + ->Arg(1000000) + ->Unit(benchmark::kMillisecond); + +template +static void BM_points_read_reflect_cpp_msgpack(benchmark::State& state) { + run( + state, + [](const Points& points) { return rfl::msgpack::write(points); }, + [](const auto& data) { return rfl::msgpack::read(data); }); +} +BENCHMARK_TEMPLATE(BM_points_read_reflect_cpp_msgpack, rfl::Processors<>) + ->Arg(1000) + ->Arg(1000000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_points_read_reflect_cpp_msgpack, rfl::DefaultIfMissing) + ->Arg(1000) + ->Arg(1000000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_points_read_reflect_cpp_msgpack, rfl::NoFieldNames) + ->Arg(1000) + ->Arg(1000000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_points_read_reflect_cpp_msgpack, rfl::NoFieldNames, + rfl::DefaultIfMissing) + ->Arg(1000) + ->Arg(1000000) + ->Unit(benchmark::kMillisecond); + +template +static void BM_points_read_reflect_cpp_yas(benchmark::State& state) { + run( + state, + [](const Points& points) { return rfl::yas::write(points); }, + [](const auto& data) { return rfl::yas::read(data); }); +} +BENCHMARK_TEMPLATE(BM_points_read_reflect_cpp_yas, rfl::Processors<>) + ->Arg(1000) + ->Arg(1000000) + ->Unit(benchmark::kMillisecond); + +} // namespace points_read diff --git a/include/rfl/parsing/NamedTupleParser.hpp b/include/rfl/parsing/NamedTupleParser.hpp index 459510649..244b34631 100644 --- a/include/rfl/parsing/NamedTupleParser.hpp +++ b/include/rfl/parsing/NamedTupleParser.hpp @@ -382,7 +382,6 @@ struct NamedTupleParser { auto set = std::array(); set.fill(false); std::vector errors; - errors.reserve(size_); const auto reader = ViewReaderType(&_r, _view, &found, &set, &errors); if constexpr (_no_field_names) { const auto err = _r.read_array(reader, _obj_or_arr); @@ -407,7 +406,6 @@ struct NamedTupleParser { const R& _r, const InputObjectOrArrayType& _obj_or_arr, NamedTupleType* _view) noexcept { std::vector errors; - errors.reserve(size_); const auto reader = ViewReaderWithDefaultType(&_r, _view, &errors); if constexpr (_no_field_names) { const auto err = _r.read_array(reader, _obj_or_arr); From 4ff84cfc049dbc59de470a8f35d33bb9f1391a83 Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Wed, 14 Oct 2026 16:47:33 +0300 Subject: [PATCH 2/2] Reserve error capacity only when field parsing fails --- include/rfl/parsing/ViewReader.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/rfl/parsing/ViewReader.hpp b/include/rfl/parsing/ViewReader.hpp index c901adab7..9082ac9e3 100644 --- a/include/rfl/parsing/ViewReader.hpp +++ b/include/rfl/parsing/ViewReader.hpp @@ -100,6 +100,9 @@ class ViewReader { std::stringstream stream; stream << "Failed to parse field '" << name << "': " << res.error().what(); + // Reserve lazily to avoid error-vector reallocations while keeping + // successful reads allocation-free. + _errors->reserve(size_); _errors->emplace_back(stream.str()); return; }