Skip to content

perf: reuse checked fields when decoding struct variants - #2040

Open
mrousavy wants to merge 1 commit into
v1from
mrousavy/fused-variant-decoding
Open

mrousavy wants to merge 1 commit into
v1from
mrousavy/fused-variant-decoding

Conversation

@mrousavy

@mrousavy mrousavy commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Cache the top-level JS field values checked while selecting a struct variant case, then decode the selected native value from that snapshot. This avoids reading those properties again in C++ variants, JNI sealed cases, and module-owned Swift structs. Ordinary typed struct calls keep their current converters.

Matching getters and proxy reads now run once and decoding uses the values observed during selection. Case order, mismatch short-circuiting and conversion-error propagation remain intact. Rejected cases create no native field values. Nested struct/collection checks retain their existing behavior. Imported Swift structs keep the previous path for compatibility with older generated dependencies; C++ and JNI detect preparation support at compile time.

First-match numeric structs take about 33% less time in the measured C++/Hermes workload; last-match structs take 24–32% less. Scalar and array controls are unchanged within the measured spread.

Case Current check + decode Prepared field snapshot Paired ratio (P10–P90)
2 fields, first case 95.6 64.1 0.670× (0.666–0.674)
8 fields, first case 365.0 243.0 0.668× (0.652–0.674)
16 fields, first case 743.1 496.0 0.665× (0.643–0.673)
16 fields, last case 775.3 530.7 0.681× (0.675–0.696)
16 fields, late rejection then last case 1,138.2 897.7 0.787× (0.776–0.802)
16 fields, unmatched/error 24,385 24,448 1.003× (0.986–1.015)
Scalar control 2.4 2.4 1.000× (0.990–1.002)
8-number array control 123.8 123.8 1.001× (0.994–1.013)

Reproducible benchmark source, raw samples and build commands: https://gist.github.com/mrousavy/4fa3d9ba78d44d8127a55269dba99d56

Benchmark method and environment

All times are ns per native variant decode, including selection and C++ result construction. Ratios pair the prepared decoder with the baseline from the same process/case/round; lower is faster. Five fresh processes ran seven measured rounds per case, giving 35 samples per option/case and 1,190 measured batches. Inputs were created outside timing. Both paths warmed up, shared one calibrated iteration count targeting a five-millisecond batch for the slower path, and ran in randomized order. A full Hermes GC preceded each batch, outside the timer. Every decoded result escaped through a compiler barrier; selected alternative and decoded-value checksums were checked outside timers.

The benchmark uses actual Nitrogen-generated native C++ structs, not a synthetic canConvert/decode lambda. Before* converters come from the exact generator at cdaf72e60; After* converters come from the prepared-field implementation. BaselineVariant.hpp is the exact old generic variant converter with only its class name changed so both implementations coexist in one executable. Structs have 2, 8 or 16 numeric fields per alternative. Inputs are immutable plain JS objects. First/last cases match their respective candidate; overlap matches both and must select the first; late rejection invalidates the first candidate's last field while the second matches; unmatched input invalidates the first candidate late and omits the second candidate's first field. Scalar and eight-number-array unions exercise unaffected decoder paths. Error cases include normal exception-message construction.

Apple M2 Pro (arm64), macOS 27.2 (26B123), Apple clang 21.0.0 (clang-2100.4.1.1), -std=c++20 -O3 -DNDEBUG. Hermes pinned commit 26fbb82fad39d9d06901adb1d6d630dacc84c877, clean-source Release build, Hades GC, HEAP_HV_64, Unicode-lite, debugger/Intl disabled. Processes ran sequentially in an exclusive no-build window. These runtime measurements cover C++ struct/variant conversion through raw JSI/Hermes. Swift/JNI implementations have separate compilation/correctness coverage; they are not timed here.

All variant benchmark cases
Case Current check + decode Prepared field snapshot Paired ratio (P10–P90)
2 fields, first case 95.6 64.1 0.670× (0.666–0.674)
2 fields, last case 134.4 102.1 0.760× (0.753–0.764)
2 fields, overlap, first wins 95.7 64.1 0.670× (0.660–0.675)
2 fields, first rejected at last field, second wins 154.2 121.9 0.792× (0.780–0.799)
2 fields, unmatched/error 23,970 24,035 1.003× (0.997–1.031)
8 fields, first case 365.0 243.0 0.668× (0.652–0.674)
8 fields, last case 402.0 283.2 0.702× (0.686–0.712)
8 fields, overlap, first wins 365.2 243.5 0.666× (0.649–0.675)
8 fields, first rejected at last field, second wins 566.1 448.5 0.790× (0.770–0.805)
8 fields, unmatched/error 24,157 24,232 1.004× (1.000–1.009)
16 fields, first case 743.1 496.0 0.665× (0.643–0.673)
16 fields, last case 775.3 530.7 0.681× (0.675–0.696)
16 fields, overlap, first wins 743.4 498.0 0.669× (0.658–0.676)
16 fields, first rejected at last field, second wins 1,138.2 897.7 0.787× (0.776–0.802)
16 fields, unmatched/error 24,385 24,448 1.003× (0.986–1.015)
Scalar control 2.4 2.4 1.000× (0.990–1.002)
8-number array control 123.8 123.8 1.001× (0.994–1.013)

Validation: Nitrogen build, typecheck and lint pass; regenerating all four test packages is deterministic across 890 files. All 127 generated Android C++ translation units pass NDK syntax checks. External C++/Hermes fixtures pass ASan+UBSan checks for ordered matching, getters/proxies, rejected cases with side effects, selected-case conversion failures and cached references surviving forced GC. Hermes itself is an uninstrumented Release build. Real generated Swift/Hermes runtime checks pass for struct/array/primitive success, once-only getters, short-circuiting, throwing getters, selected-case failures and unmatched input. Legacy external Swift converter output is checked against the previous generator. All added fixtures and benchmarks stay outside the repository.

@vercel

vercel Bot commented Nov 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
nitro-docs Skipped Skipped Nov 2, 2026 1:14pm UTC

Request Review

This branch was previously deployed

1 inactive deployment
Preview — ef08894c Deployed Nov 2, 2026 by vercel[bot]
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.

1 participant