Repository navigation
Conversation
|
Also I was thinking if a raw serialization vs reflect-cpp serialisation based on the same library benchmark would be useful in the repository. I understand that reflect-cpp does a bit more in terms of error handling vs raw serializer. But in theory perfect world implementation for the rfl::ceral::write should be as fast as generic handwritten cereal serializer. |
|
I'll also try to investigate why YAS is still so much slower, even after all my improvements that bring other formats close to baseline. |
|
While testing #744 with Linux+GCC I found that this PR decrease LTO GCC performance by ~40% and increase no-LTO by ~25%, so I guess it might be better cancel this PR even with clear win-win on clang on MacOS |
Important edit: while testing #744 with Linux+GCC I found that this PR decrease LTO GCC performance by ~40% and increase no-LTO by ~25%, so I guess it might be better cancel this PR even with clear win-win on clang on MacOS
Reading vectors of structs still leaves small parser helpers out of line and error-handling code mixed into hot functions in optimized builds. Explicitly inline scalar parsing and vector-element parsing, and mark eight existing error branches as unlikely. This improves generated code layout and slightly reduces code size, with read improvements both without LTO and with ThinLTO.
The shared-parser changes are:
RFL_ALWAYS_INLINEtoParserBasicType::read.VectorReader's local parsing lambda into an explicitly inlined privateparsemethod. The insertion logic is unchanged.[[unlikely]]to failed field reads, failed object/array reads, and collected-error checks inViewReaderandNamedTupleParser.Benchmarks
Compare with upstream
mainat10fb6bb. The checked-inparser_inliningread benchmarks cover 100,000 records with either two or sixteenfloatfields; this PR adds matching YAS cases. A local extended harness uses those same schemas to measure reads and writes, and adds native serializer references for all four formats.A separate local Dataset-only harness measures all four formats using a nested
Dataset: 500 groups, each containing 3,000Point2 { double x; double y; }values and 3,000uint64_tIDs. Dataset rows below come from this new run; record rows retain their original measurements. Both gains and losses are subject to the same 3% cutoff.All reflect-cpp cases use ordinary
rfl::<format>::write/read, without custom parsers or field annotations. Native references use Cereal portable binary archives, YAS binary archives, yyjson directly, and MessagePack C directly. The native JSON and MessagePack converters are handwritten for these schemas, retaining field names and checking required fields and numeric types; they are reference implementations, not replacements for reflect-cpp's general parsing and error-aggregation semantics.Input generation and equality checks are outside the timers. Reads include byte decoding, C++ object construction, and destruction. Writes include encoding into an owned byte container and its destruction. Cereal uses the same memory-stream construction and copies on both sides. YAS uses memory buffers; JSON and MessagePack include their DOM/tree conversion. Read fixtures are serialized before timing. No filesystem I/O is timed.
Apple Clang 21, macOS arm64, C++20, Release
-O3 -DNDEBUG. Median CPU times from six runs per configuration, with at least 0.1 s warmup, 40 measured iterations per record case, and 10 measured iterations per Dataset case. Baseline, patched, and native configurations are rotated and reversed between runs. ThinLTO is enabled on benchmark code and the reflect-cpp core, including yyjson and the final link; MessagePack C and Google Benchmark are the same separately built libraries in all configurations.All times are milliseconds. Gain = old / new; above 1.00× is faster. Native serializer comparisons are provided separately below the PR description. Changes below 3% in measured CPU time are treated as noise. A row is shown only if the absolute change reaches 3% in at least one build configuration; an em dash in a Gain cell means that configuration is below the cutoff. The cutoff uses unrounded measurements.
Reads
The original record fixtures and all four new Dataset fixtures are byte-identical across all six baseline/patched/native and LTO/no-LTO configurations. Every reader successfully decodes the baseline output and reproduces every field. These are local measurements; gains depend on compiler, link configuration, schema, and workload. The patch changes read-side code only; writes are measured as a control.
Generated code layout and size
The compiler has more freedom to optimize the successful path after helper inlining and error-branch hints. The original four-format, read-only record benchmark executable (without the extended native/write/nested cases above) has a smaller machine-code section in both configurations:
__text__textThese are executable
__textsection sizes measured withllvm-objdump, not total file sizes or debug information. The executable includes the shared benchmark infrastructure and verification driver in both variants. Code-size results may differ for other programs.Validation
Run the checked-in reflect-cpp record read cases with (the local native, write, and nested comparisons above use an extended harness):
For a local LTO comparison, build the same sources separately with
CMAKE_INTERPROCEDURAL_OPTIMIZATION=OFFandONin Release mode.Informational comparison: reflect-cpp vs native serializers
This is context for the absolute performance of reflect-cpp, not evidence for this PR’s improvement. New is the build with this PR; Native uses ordinary Cereal/YAS archives or the C library plus a handwritten schema converter for JSON/MessagePack. Ratio = New / Native: above 1× means reflect-cpp takes more time.
Median CPU times from six rotated/reversed runs per configuration, with 0.1 s warmup and at least 0.25 s measured CPU time per case. Compiler, optimization flags, libraries, timed ownership/copying, and input generation follow the methodology above. All 12 fixtures are byte-identical across old-native, revised-native, and patched-reflect-cpp builds, both with and without ThinLTO; every reader reproduces the baseline data. Separate semantic checks cover reordered/duplicate/unknown fields and invalid inputs.
Reads
Writes