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 |
|
@Altren OK, if you think that it is better to cancel the PR, then do so. |
|
I'll try to make it good on lixux+gcc and macos+clang. Doing experiments takes much more time, but I think I can get to a point where reflect-cpp is within 20-30% of perfect result, unlike now where f.e. native YAS can be 80% faster. @liuzicheng1987 Another topic. What do you think about about adding perfect result benchmark into repo? My second message in the PR |
|
@Altren yes, I think adding a "perfect result benchmark" for cereal, yas, etc would be a very good idea. We have something like this for JSON and it would be interesting to see that for these formats as well. |
|
@Altren by the way, I would also like to do a release quite today. I will do a 0.26.0 release for now, but I think we should move this out of beta (ergo a 1.0.0 release) once I have made a decision on how to proceed with #719. So, in other words, we will have another release quite soon. I am also going to move the repo to a different account, but that shouldn't affect your work too much. |
|
@Altren But I think that performance improvements should be a major goal moving forward. We are still slower than YYJSON on most benchmarks. Not by much, so it wasn't something I cared about in the past, but I think now is the time to really close that gap. |
7263f45 to
517cf6e
Compare
|
I fugured out what was the issue and also added one more commit into this branch. Would be great if it will get into release |
|
The Cereal LTO write slowdown is sensitive to executable layout. The generated serialization loops retain the same instructions and calls. Relinking the same compiled objects with a shared writer-function order removes the main/new timing gap in both cases. This control establishes a role for linked layout, not extra serialization work. The linker order is diagnostic only and is not part of the patch; the original timing observations remain valid for their executable. Differences below 3% are treated as noise. The current revision was also checked against the earlier GCC regressions:
|
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