Skip to content

Optimize positional field dispatch with compile-time indices - #741

Merged
liuzicheng1987 merged 6 commits into
munich-data-engineering:mainfrom
Altren:perf/cereal-field-dispatch
Oct 16, 2026
Merged

liuzicheng1987 merged 6 commits into
munich-data-engineering:mainfrom
Altren:perf/cereal-field-dispatch

Conversation

@Altren

@Altren Altren commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Positional object readers already know which field comes next, but currently pass a runtime index through a scan of all reflected fields. Dispatch directly to the selected field instead.

  • Cereal and YAS pass compile-time indices to a shared ViewReader overload that reuses the existing assignment path.
  • Boost.Serialization, Avro, and Cap’n Proto use compile-time indices when the input field count matches the target. Differing counts and readers without a compile-time size retain the existing runtime path.
  • NoFieldNames array callbacks retain a runtime position, so their two shared view readers use a constexpr table of field-specific functions. Both ordinary and DefaultIfMissing paths retain bounds checks, missing-field handling, validation, error collection, and cleanup.
  • Serialization and wire formats are unchanged. Each additional format and the shared NoFieldNames change are separate commits.

The NoFieldNames change is covered in JSON, MessagePack, CBOR, FlexBuffers, UBJSON, Cereal, YAS, Boost.Serialization, and Generic. It does not enable unsupported NoFieldNames combinations: Avro's ordinary nested-record writer fails before reading, Cap’n Proto requires a struct root, and BSON/XML/TOML/YAML explicitly reject this processor.

Validation:

  • New tests cover field order, multiple validation errors, cleanup after failures, truncated input, empty objects, matching and differing field counts, runtime-index-only readers, defaults, and missing/extra array values.
  • All 16 benchmark fixtures have identical before/after bytes, and the optimized readers successfully read baseline output.
  • All 160 repeated benchmark case runs pass. All 18 field-dispatch cases, including the matching Cereal 2-/16-field workloads, run successfully in the normal all-format benchmark executable.

The new benchmarks read a struct containing a vector of 100,000 deterministic records with either 2 or 16 float fields. Input generation, serialization, schema construction, and equality checks are outside the timer; parsing and output destruction are timed. These use ordinary reflect-cpp APIs, with NoFieldNames and DefaultIfMissing only in their labeled cases.

Both variants include #738, the reserve-capacity changes from #739 (ce5db49). This isolates each format's dispatch change; bulk-vector I/O and adapter inlining are excluded. The PR branch itself remains based on upstream main and does not depend on #739.

Reader / mode 2-field speedup 16-field before (ms) 16-field after (ms) 16-field speedup 16-field serialized bytes
Cereal 1.12× 41.285 14.572 2.83× 6,400,009
YAS 1.17× 36.956 9.480 3.90× 6,400,015
Boost.Serialization 0.96× 39.236 12.648 3.10× 7,200,016
Avro 1.03× 53.737 28.463 1.89× 6,400,004
Cap’n Proto 1.09× 68.095 37.402 1.82× 4,047,530
JSON, NoFieldNames 1.14× 45.268 18.202 2.49× 8,083,343
JSON, NoFieldNames + DefaultIfMissing 1.13× 42.134 16.699 2.52× 8,083,343
MessagePack, NoFieldNames 1.10× 43.159 16.467 2.62× 8,300,006
MessagePack, NoFieldNames + DefaultIfMissing 1.09× 40.643 13.911 2.92× 8,300,006

Wider records benefit substantially more. The two-field Boost case is about 4% slower (2.505 → 2.611 ms); this is a workload-dependent tradeoff, not a universal speedup.

Compile-time and code-size tradeoffs for the extension commits: three alternating release compilations of each new benchmark source file (after warmup, without PCH or linking) measured approximately unchanged compile time for YAS and NoFieldNames, +4.2% for Boost, +2.9% for Avro, and +4.0% for Cap’n Proto. Avro’s object text section grew 15.1%; other text-section changes were within 3%. NoFieldNames object-file size grew 16.3%, while its text-section size was essentially unchanged. These are source-file measurements, not full-project build or final executable sizes.

The PR introduces the shared test helper, shared benchmark helper, explicit compile-time dispatch test, and matching Cereal 2-/16-field benchmark. Added the field_dispatch cases to the existing all-format benchmark executable and Linux/macOS/Windows benchmark summaries.

./build/benchmarks/all/reflect-cpp-all-format-benchmarks \
  --benchmark_filter='field_dispatch|points_read_reflect_cpp_cereal' \
  --benchmark_min_warmup_time=0.1 \
  --benchmark_min_time=0.2s \
  --benchmark_repetitions=5

Georgii Evmenov added 6 commits October 15, 2026 04:03
Select the known field directly through the existing ViewReader assignment path instead of scanning every field for each runtime index. Preserve validation, error collection, and cleanup behavior.

Add regression coverage for multiple field errors, truncated input cleanup, empty objects, and runtime-index reader compatibility.
Replace the per-element field scan with a constexpr table of field-specific readers, preserving bounds checks, defaults, validation, and cleanup. Cover supported serialization formats and Generic; benchmark JSON and MessagePack with and without DefaultIfMissing.
@liuzicheng1987

Copy link
Copy Markdown
Collaborator

Thanks for the contribution.

@liuzicheng1987
liuzicheng1987 merged commit 8866cac into munich-data-engineering:main Oct 16, 2026
176 checks passed
@Altren
Altren deleted the perf/cereal-field-dispatch branch October 17, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants