From 085e3d72b8e2561c49f0bea7f79b68226862f243 Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Thu, 15 Oct 2026 16:45:15 +0300 Subject: [PATCH 1/2] Inline frequently used parsing functions --- .github/workflows/linux.yaml | 1 + .github/workflows/macos.yaml | 1 + .github/workflows/windows.yaml | 1 + benchmarks/all/parser_inlining.hpp | 76 ++++++++++++++++++++++ benchmarks/all/parser_inlining_cereal.cpp | 19 ++++++ benchmarks/all/parser_inlining_json.cpp | 17 +++++ benchmarks/all/parser_inlining_msgpack.cpp | 19 ++++++ include/rfl/cereal/Reader.hpp | 9 ++- include/rfl/internal/always_inline.hpp | 12 ++++ include/rfl/internal/cpp20/Result.hpp | 3 +- include/rfl/parsing/NamedTupleParser.hpp | 22 ++++--- include/rfl/parsing/Parser_default.hpp | 6 +- include/rfl/parsing/VectorReader.hpp | 3 +- include/rfl/parsing/ViewReader.hpp | 10 +-- 14 files changed, 177 insertions(+), 22 deletions(-) create mode 100644 benchmarks/all/parser_inlining.hpp create mode 100644 benchmarks/all/parser_inlining_cereal.cpp create mode 100644 benchmarks/all/parser_inlining_json.cpp create mode 100644 benchmarks/all/parser_inlining_msgpack.cpp create mode 100644 include/rfl/internal/always_inline.hpp diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index 1b11ed0ec..96a446a58 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -120,6 +120,7 @@ jobs: echo '## Benchmarks across different formats' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_read >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=parser_inlining >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 40ab577d1..f38702758 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -62,6 +62,7 @@ jobs: echo '## Benchmarks across different formats' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_read >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=parser_inlining >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 14ca7b2fc..2ecc45649 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -61,6 +61,7 @@ jobs: echo '## Benchmarks across different formats' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_read >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=parser_inlining >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY diff --git a/benchmarks/all/parser_inlining.hpp b/benchmarks/all/parser_inlining.hpp new file mode 100644 index 000000000..2ce9f92c7 --- /dev/null +++ b/benchmarks/all/parser_inlining.hpp @@ -0,0 +1,76 @@ +#ifndef BENCHMARK_PARSER_INLINING_HPP_ +#define BENCHMARK_PARSER_INLINING_HPP_ +#include + +#include +#include +namespace parser_inlining { +struct Record2 { + float field0 = 0; + float field1 = 1; + bool operator==(const Record2&) const = default; +}; +struct Record16 { + float field0 = 0; + float field1 = 1; + float field2 = 2; + float field3 = 3; + float field4 = 4; + float field5 = 5; + float field6 = 6; + float field7 = 7; + float field8 = 8; + float field9 = 9; + float field10 = 10; + float field11 = 11; + float field12 = 12; + float field13 = 13; + float field14 = 14; + float field15 = 15; + bool operator==(const Record16&) const = default; +}; +template +struct Data { + std::vector records; + bool operator==(const Data&) const = default; +}; +template +auto make_data(std::size_t size) { + Data data; + data.records.resize(size); + for (std::size_t i = 0; i < size; ++i) { + data.records[i].field0 = static_cast(i) * 0.5f; + data.records[i].field1 = -static_cast(i) * 0.25f; + } + return data; +} +template +void run(benchmark::State& state, Write write, Read read) { + const auto input = make_data(state.range(0)); + const auto bytes = write(input); + { + const auto result = read(bytes); + if (!result) { + state.SkipWithError(result.error().what()); + return; + } + if (*result != input) { + state.SkipWithError("Fields changed during round trip"); + return; + } + } + for (auto _ : state) { + auto result = read(bytes); + if (!result) { + state.SkipWithError(result.error().what()); + break; + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * state.range(0)); + state.SetBytesProcessed(state.iterations() * + static_cast(bytes.size())); + state.counters["serialized_bytes"] = static_cast(bytes.size()); +} +} // namespace parser_inlining +#endif diff --git a/benchmarks/all/parser_inlining_cereal.cpp b/benchmarks/all/parser_inlining_cereal.cpp new file mode 100644 index 000000000..83ada2287 --- /dev/null +++ b/benchmarks/all/parser_inlining_cereal.cpp @@ -0,0 +1,19 @@ +#include + +#include "parser_inlining.hpp" +namespace parser_inlining { +template +void BM_parser_inlining_cereal(benchmark::State& state) { + run( + state, [&](const auto& input) { return rfl::cereal::write(input); }, + [&](const auto& bytes) { + return rfl::cereal::read>(bytes); + }); +} +BENCHMARK_TEMPLATE(BM_parser_inlining_cereal, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_cereal, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace parser_inlining diff --git a/benchmarks/all/parser_inlining_json.cpp b/benchmarks/all/parser_inlining_json.cpp new file mode 100644 index 000000000..c1dca3037 --- /dev/null +++ b/benchmarks/all/parser_inlining_json.cpp @@ -0,0 +1,17 @@ +#include + +#include "parser_inlining.hpp" +namespace parser_inlining { +template +void BM_parser_inlining_json(benchmark::State& state) { + run( + state, [&](const auto& input) { return rfl::json::write(input); }, + [&](const auto& bytes) { return rfl::json::read>(bytes); }); +} +BENCHMARK_TEMPLATE(BM_parser_inlining_json, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_json, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace parser_inlining diff --git a/benchmarks/all/parser_inlining_msgpack.cpp b/benchmarks/all/parser_inlining_msgpack.cpp new file mode 100644 index 000000000..c702a65a4 --- /dev/null +++ b/benchmarks/all/parser_inlining_msgpack.cpp @@ -0,0 +1,19 @@ +#include + +#include "parser_inlining.hpp" +namespace parser_inlining { +template +void BM_parser_inlining_msgpack(benchmark::State& state) { + run( + state, [&](const auto& input) { return rfl::msgpack::write(input); }, + [&](const auto& bytes) { + return rfl::msgpack::read>(bytes); + }); +} +BENCHMARK_TEMPLATE(BM_parser_inlining_msgpack, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_msgpack, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace parser_inlining diff --git a/include/rfl/cereal/Reader.hpp b/include/rfl/cereal/Reader.hpp index 85d1ab09d..42cbe6655 100644 --- a/include/rfl/cereal/Reader.hpp +++ b/include/rfl/cereal/Reader.hpp @@ -13,6 +13,7 @@ #include "../Result.hpp" #include "../Vectorstring.hpp" #include "../always_false.hpp" +#include "../internal/always_inline.hpp" #include "../internal/is_literal.hpp" #include "../parsing/schemaful/IsSchemafulReader.hpp" @@ -63,7 +64,8 @@ struct Reader { /// @param _var The Cereal archive variable containing the value /// @return A Result containing the converted value or an error template - rfl::Result to_basic_type(const InputVarType& _var) const noexcept { + RFL_ALWAYS_INLINE rfl::Result to_basic_type( + const InputVarType& _var) const noexcept { try { if constexpr (internal::is_literal_v) { std::string str; @@ -168,8 +170,9 @@ struct Reader { /// @param _obj The Cereal object structure /// @return std::nullopt on success, or an Error if reading fails template - std::optional read_object(const ObjectReader& _object_reader, - const InputObjectType& _obj) const noexcept { + RFL_ALWAYS_INLINE std::optional read_object( + const ObjectReader& _object_reader, + const InputObjectType& _obj) const noexcept { try { for (size_t i = 0; i < ObjectReader::size(); ++i) { _object_reader.read(i, InputVarType{_obj.archive_}); diff --git a/include/rfl/internal/always_inline.hpp b/include/rfl/internal/always_inline.hpp new file mode 100644 index 000000000..97e19966d --- /dev/null +++ b/include/rfl/internal/always_inline.hpp @@ -0,0 +1,12 @@ +#ifndef RFL_INTERNAL_ALWAYS_INLINE_HPP_ +#define RFL_INTERNAL_ALWAYS_INLINE_HPP_ + +#if defined(_MSC_VER) +#define RFL_ALWAYS_INLINE __forceinline +#elif defined(__GNUC__) || defined(__clang__) +#define RFL_ALWAYS_INLINE inline __attribute__((always_inline)) +#else +#define RFL_ALWAYS_INLINE inline +#endif + +#endif diff --git a/include/rfl/internal/cpp20/Result.hpp b/include/rfl/internal/cpp20/Result.hpp index 1e210e967..6d516e4f0 100644 --- a/include/rfl/internal/cpp20/Result.hpp +++ b/include/rfl/internal/cpp20/Result.hpp @@ -8,6 +8,7 @@ #include #include "../Error.hpp" +#include "../always_inline.hpp" namespace rfl::internal::cpp20 { @@ -331,7 +332,7 @@ class Result { /// @note This overload is for rvalue references (&&), so the contained value /// is moved. template - auto transform(const F& _f) && { + RFL_ALWAYS_INLINE auto transform(const F& _f) && { /// Result_U is expected to be of type Result. using U = std::invoke_result_t; if (success_) { diff --git a/include/rfl/parsing/NamedTupleParser.hpp b/include/rfl/parsing/NamedTupleParser.hpp index 244b34631..14d3159ce 100644 --- a/include/rfl/parsing/NamedTupleParser.hpp +++ b/include/rfl/parsing/NamedTupleParser.hpp @@ -9,6 +9,7 @@ #include "../NamedTuple.hpp" #include "../Result.hpp" +#include "../internal/always_inline.hpp" #include "../internal/default_if_missing_v.hpp" #include "../internal/has_default_val_v.hpp" #include "../internal/has_reflection_method_v.hpp" @@ -145,10 +146,10 @@ struct NamedTupleParser { * @return A pair containing a boolean array indicating which fields were * found and an optional error. */ - static std::pair, - std::optional> - read_view(const R& _r, const InputVarType& _var, - NamedTuple* _view) noexcept { + static RFL_ALWAYS_INLINE + std::pair, std::optional> + read_view(const R& _r, const InputVarType& _var, + NamedTuple* _view) noexcept { if constexpr (_no_field_names) { auto arr = _r.to_array(_var); if (!arr) [[unlikely]] { @@ -321,7 +322,7 @@ struct NamedTupleParser { /// Generates error messages for when fields are missing. template - static void handle_one_missing_field( + static RFL_ALWAYS_INLINE void handle_one_missing_field( const std::array& _found, const NamedTupleType& _view, std::array* _set, std::vector* _errors) noexcept { @@ -357,7 +358,7 @@ struct NamedTupleParser { /// Generates error messages for when fields are missing. template - static void handle_missing_fields( + static RFL_ALWAYS_INLINE void handle_missing_fields( const std::array& _found, const NamedTupleType& _view, std::array* _set, std::vector* _errors, std::integer_sequence) noexcept { @@ -373,10 +374,11 @@ struct NamedTupleParser { } } - static std::pair, - std::optional> - read_object_or_array(const R& _r, const InputObjectOrArrayType& _obj_or_arr, - NamedTupleType* _view) noexcept { + static RFL_ALWAYS_INLINE + std::pair, std::optional> + read_object_or_array(const R& _r, + const InputObjectOrArrayType& _obj_or_arr, + NamedTupleType* _view) noexcept { auto found = std::array(); found.fill(false); auto set = std::array(); diff --git a/include/rfl/parsing/Parser_default.hpp b/include/rfl/parsing/Parser_default.hpp index 97e9ee803..a0517925b 100644 --- a/include/rfl/parsing/Parser_default.hpp +++ b/include/rfl/parsing/Parser_default.hpp @@ -7,6 +7,7 @@ #include "../Result.hpp" #include "../always_false.hpp" #include "../internal/HasFromReflectionMethod.hpp" +#include "../internal/always_inline.hpp" #include "../internal/default_if_missing_v.hpp" #include "../internal/has_default_val_v.hpp" #include "../internal/has_reflection_method_v.hpp" @@ -87,7 +88,7 @@ struct Parser { * @param _var The input variable to read from. * @return A Result containing the parsed value or an error. */ - static auto read(const R& _r, const InputVarType& _var) { + static RFL_ALWAYS_INLINE auto read(const R& _r, const InputVarType& _var) { if constexpr (internal::is_basic_type_v) { return ParserBasicType::read(_r, _var); @@ -686,7 +687,8 @@ struct Parser { /// of the struct in which we then write the individual fields using views /// and placement new. This is how we deal with the fact that some fields /// might not be default-constructible. - static Result read_struct(const R& _r, const InputVarType& _var) { + static RFL_ALWAYS_INLINE Result read_struct(const R& _r, + const InputVarType& _var) { alignas(T) unsigned char buf[sizeof(T)]{}; auto ptr = internal::ptr_cast(&buf); auto view = ProcessorsType::template process(to_view(*ptr)); diff --git a/include/rfl/parsing/VectorReader.hpp b/include/rfl/parsing/VectorReader.hpp index c7abd90d3..95ef831b0 100644 --- a/include/rfl/parsing/VectorReader.hpp +++ b/include/rfl/parsing/VectorReader.hpp @@ -6,6 +6,7 @@ #include #include "../Result.hpp" +#include "../internal/always_inline.hpp" #include "Parser_base.hpp" #include "is_inplace_vector.hpp" #include "is_map_like.hpp" @@ -36,7 +37,7 @@ class VectorReader { * @param _var The input variable to read from. * @return An optional error. */ - std::optional read(const InputVarType& _var) const { + RFL_ALWAYS_INLINE std::optional read(const InputVarType& _var) const { const auto parse = [this](const InputVarType& _var) { if constexpr (is_map_like_v) { return get_pair(_var); diff --git a/include/rfl/parsing/ViewReader.hpp b/include/rfl/parsing/ViewReader.hpp index 9082ac9e3..7b7f70f65 100644 --- a/include/rfl/parsing/ViewReader.hpp +++ b/include/rfl/parsing/ViewReader.hpp @@ -9,6 +9,7 @@ #include #include "../Tuple.hpp" +#include "../internal/always_inline.hpp" #include "../internal/is_array.hpp" #include "../internal/no_extra_fields_v.hpp" #include "Parser_base.hpp" @@ -81,11 +82,10 @@ class ViewReader { } template - static void assign_if_field_matches(const R& _r, - const auto _current_name_or_index, - const auto& _var, auto* _view, - auto* _errors, auto* _found, auto* _set, - bool* _already_assigned) { + static RFL_ALWAYS_INLINE void assign_if_field_matches( + const R& _r, const auto _current_name_or_index, const auto& _var, + auto* _view, auto* _errors, auto* _found, auto* _set, + bool* _already_assigned) { using FieldType = tuple_element_t; using OriginalType = typename FieldType::Type; using T = From d16d23852e7b7a3af1953e6b7945b7f444d0c246 Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Thu, 15 Oct 2026 19:50:52 +0300 Subject: [PATCH 2/2] Fix GCC 12 warning in inlined struct parsing --- include/rfl/parsing/Parser_default.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/rfl/parsing/Parser_default.hpp b/include/rfl/parsing/Parser_default.hpp index a0517925b..3a92a86cf 100644 --- a/include/rfl/parsing/Parser_default.hpp +++ b/include/rfl/parsing/Parser_default.hpp @@ -691,7 +691,8 @@ struct Parser { const InputVarType& _var) { alignas(T) unsigned char buf[sizeof(T)]{}; auto ptr = internal::ptr_cast(&buf); - auto view = ProcessorsType::template process(to_view(*ptr)); + auto raw_view = to_view(*ptr); + auto view = ProcessorsType::template process(std::move(raw_view)); using ViewType = std::remove_cvref_t; const auto [set, err] = Parser::read_view(_r, _var, &view);