Skip to content

Allow monomorphization for reducers and more - #5610

Draft
lisandroct wants to merge 2 commits into
masterfrom
lisandro/allow-monomorphization-for-reducers-and-more
Draft

Allow monomorphization for reducers and more#5610
lisandroct wants to merge 2 commits into
masterfrom
lisandro/allow-monomorphization-for-reducers-and-more

Conversation

@lisandroct

@lisandroct lisandroct commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Description of Changes

This PR updates the C# bindings codegen/runtime so generated module exports can dispatch directly to statically-known generic reducer, procedure, HTTP handler, view, and anonymous-view entrypoints when building with Native AOT-LLVM.

The runtime now keeps generic static caches for generated dispatchers and exposes generic call paths that avoid relying only on indexed interface collections. The generated FFI forwarding methods switch on the host-provided function/view id and call the matching generic runtime entrypoint, while preserving the existing id-based fallback path.

For NativeAOT, the previous exported entrypoints always flowed through non-generic runtime methods such as Module.__call_reducer__(id, ...), which then indexed into an IReducer list and invoked through the interface. That shape hides the concrete generated reducer/handler/view type from the AOT compiler at the call site.

The generated exports now switch on the function id in generated code and call generic runtime methods such as Module.__call_reducer__<SomeReducer>(...). Because the concrete generated type is present as a generic type argument, NativeAOT can compile a specialized instantiation for that reducer/handler/view. In practice, this gives the AOT compiler a monomorphic call path: it can see the exact dispatcher type, use the static generic cache for that type, and avoid part of the dynamic interface/list dispatch shape that was previously required.

All of this gives extra optimization opportunities in AOT output because the compiler no longer has to treat every reducer invocation as the same opaque interface call.

Generated code snapshots were updated for the new sealed dispatcher classes and AOT forwarding shape.

API and ABI breaking changes

No public API or module ABI breaking changes. The change is internal to C# generated code and runtime dispatch behavior.

Expected complexity level and risk

3

The main risk is that generated dispatch ids must stay aligned with registration order for reducers, procedures, HTTP handlers, named views, and anonymous views. The implementation preserves fallback id-based dispatch, but it touches the C# module FFI path used by NativeAOT/.NET 10 builds, so reviewers should pay close attention to ordering and parity across all generated dispatcher categories.

Testing

  • Ran tests and they're all passing
  • Ran targeted C# NativeAOT benchmark comparisons against master
  • Repeated benchmark runs to estimate noise/spread

Benchmark Results

Benchmarks were run through the existing harness using C# stdb_module/csharp NativeAOT/.NET 10 module builds.

Each row below reports the median across three runs per branch. The range column is the min/max spread across those three runs, so it gives a rough estimate of run-to-run noise.

Benchmark master median, range PR median, range PR vs master
large args 64KiB 43.768 us, 8.1% 44.100 us, 2.5% +0.8%
print_bulk lines=1 7.155 us, 22.4% 6.902 us, 9.2% -3.5%
print_bulk lines=100 60.714 us, 7.1% 57.402 us, 5.1% -5.5%
print_bulk lines=1000 521.942 us, 8.3% 508.555 us, 4.6% -2.6%
circles load=10 33.612 ms, 2.3% 33.767 ms, 0.5% +0.5%
circles load=100 33.789 ms, 2.8% 33.733 ms, 2.7% -0.2%
ia_loop load=10 10.550 ms, 5.1% 10.529 ms, 1.3% -0.2%
ia_loop load=100 26.880 ms, 5.1% 26.903 ms, 3.8% +0.1%
filter string index 44.079 us, 3.5% 44.228 us, 1.9% +0.3%
filter u64 index 20.212 us, 3.0% 20.309 us, 2.7% +0.5%
insert u32/u64/str unique 266.848 us, 4.1% 265.643 us, 5.2% -0.5%
insert u32/u64/str btree 390.298 us, 0.6% 364.261 us, 4.4% -6.7%
iterate u32/u64/str 44.058 us, 1.5% 44.148 us, 3.3% +0.2%
insert u32/u64/u64 unique 167.317 us, 6.9% 180.238 us, 5.7% +7.7%
insert u32/u64/u64 btree 258.715 us, 7.1% 258.304 us, 6.8% -0.2%
iterate u32/u64/u64 14.932 us, 3.4% 14.979 us, 2.0% +0.3%

Overall, the PR appears to reduce spread in most of the reducer-heavy special benchmarks. The clearest improvements are:

  • print_bulk lines=100: about 5.5% faster
  • print_bulk lines=1000: about 2.6% faster
  • insert u32/u64/str btree: about 6.7% faster
  • print_bulk lines=1: about 3.5% faster, but master had very high spread on this benchmark

Most game, filter, and iterate workloads are effectively flat. These are dominated more by database/query work than by reducer dispatch overhead.

The main negative result in the three-run aggregate was insert u32/u64/u64 unique, which showed +7.7%. I investigated it separately with five focused runs of only that benchmark:

Set master PR Delta
focused median 164.408 us 168.442 us +2.5%
focused mean 166.759 us 168.588 us +1.1%
run spread 6.1% 1.5%

The focused rerun does not confirm a meaningful regression. The best estimate is roughly flat, possibly +1-2.5%, while master’s own focused run spread was about 6.1%.

@lisandroct
lisandroct marked this pull request as draft July 28, 2026 19:38
@lisandroct
lisandroct force-pushed the lisandro/allow-monomorphization-for-reducers-and-more branch from d6c0f77 to dc5c78d Compare August 13, 2026 15:05
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