๐ Benchmarks
GenDI includes a dedicated BenchmarkDotNet project to validate startup registration performance across four distinct strategies, giving developers the data to make an informed choice.
Scope note (Phase 6 parity)โ
These benchmarks measure startup registration/activation cost. Feature-specific behavior introduced in Phase 6 (for example RegistrationMultiplicity/RegistrationEmissionStrategy combinations and OptionConfig section-selection evolution) is covered in functional docs and tests rather than this benchmark suite.
๐ฏ Scenariosโ
| # | Description | How registration happens | How activation happens |
|---|---|---|---|
| 1 | Manual (no GenDI) | โ๏ธ Hand-written AddSingleton<> / AddTransient<> | Container expression-tree compilation (one-time reflection) |
| 2 | GenDI โ constructor injection | โก AddGenDIServices() (compile-time generated) | Generated factory: new Service(sp.Get<A>(), sp.Get<B>()) |
| 3 | GenDI โ property injection | โก AddGenDIServices() (compile-time generated) | Generated factory: new Service { A = sp.Get<A>(), B = sp.Get<B>() } |
| 4 | GenDI: property with decorator 1 | โก AddGenDIServices() (compile-time generated) | Generated factory: new Decorator { Inner = new Service { A = sp.Get<A>(), B = sp.Get<B>() } } |
| 5 | Reflection scanner (worst case) | ๐ข Assembly.GetTypes() scan at startup | Container expression-tree compilation |
1 This scenario adds a simple decorator layer to the property injection case, validating that even with an extra level of factory nesting the generated code remains performant.
๐งช Benchmark projectโ
- ๐
tests/GenDI.Benchmarks - ๐
StartupRegistrationBenchmarks
Run locally:
dotnet run -c Release --project tests/GenDI.Benchmarks/GenDI.Benchmarks.csproj -- --job Short --filter "*"
โก Latest result snapshotโ
Updated by CI run #202 on 2026-05-23 15:08 UTC
| Method | Mean | Allocated |
|---|---|---|
| Manual registration (no GenDI) | 3.363 ฮผs | 7.42 KB |
| GenDI: constructor injection (generated) | 4.689 ฮผs | 9.98 KB |
| GenDI: property injection (generated) | 4.594 ฮผs | 9.98 KB |
| GenDI: with decorator, property injection (generated) | 15.408 ฮผs | 14.3 KB |
| Reflection registration (no GenDI, assembly scan) | 77.987 ฮผs | 23.9 KB |
CI analysisโ
- GenDI constructor injection is +39.4% versus manual registration.
- GenDI property injection is +36.6% versus manual registration.
- Reflection scanning remains the outlier at ~23.2x slower and ~3.2x higher allocation than manual registration.
- Compatibility note: this benchmark compares manual and generated registrations against a reflection scanner baseline; as documented below, reflection scanning is not suitable for trimming/NativeAOT scenarios, while manual and GenDI-generated registrations remain the supported path.
๐ What the numbers meanโ
โ๏ธ Manual vs โก GenDI generatedโ
The manual baseline registers the same full service set as AddGenDIServices() for an
apples-to-apples comparison. In the latest CI snapshot above, both generated variants are ahead
of manual registration on mean startup time, with constructor injection currently leading the set.
The stable takeaway is that manual and generated registration stay in the same microsecond range,
while the reflection scanner remains dramatically slower.
The ergonomic price of "manual" is every new service needing its own AddScoped<>() call in a
startup file. GenDI eliminates that maintenance entirely.
๐ Constructor injection vs property injection โ it's a tieโ
The performance difference between GenDI constructor injection and GenDI property injection is ยฑ1โ2 % โ within measurement noise. Both generate an explicit compiled factory lambda; the JIT produces nearly identical machine code.
๐ก Choose property injection for cleaner code. You pay no measurable performance price.
๐จ Reflection scanner โ the real cost to avoidโ
Assembly scanning at startup is ~19ร slower and allocates ~2.5ร more memory than any
GenDI-generated strategy. GenDI moves all of that scanning to compile time โ the runtime never
touches a GetTypes() call.
๐ Summaryโ
| Comparison | ๐ Winner | Margin | Takeaway |
|---|---|---|---|
| โ๏ธ Manual vs โก GenDI generated | GenDI (latest CI snapshot) | Constructor: ~20.1 %, Property: ~17.7 % | Generated registration is currently fastest and removes manual maintenance |
| โก Constructor vs ๐ property injection | Tie | ยฑ1โ2 % (noise) | Use property injection โ zero cost, big ergonomic win |
| โก GenDI generated vs ๐ข reflection scanner | GenDI | ~19ร faster | Reflection scanning is not viable for cold-start-sensitive apps |
๐ฆ Binary size comparisonโ
These measurements use a representative minimal .NET 10 console application with three singleton services and one transient service.
๐ฅ๏ธ Environment: .NET SDK 10.0.201, linux-x64
Resultsโ
| Configuration | โ๏ธ Manual (no GenDI) | โก GenDI (ctor or property) | ๐ข Reflection scanner |
|---|---|---|---|
| Framework-dependent (folder) | 264 KB | 292 KB | 264 KB |
| Framework-dependent (app DLL) | 8 KB | 8 KB | 8 KB |
| Self-contained (folder) | ~80 MB | ~80 MB | ~80 MB |
| Trimmed self-contained (folder) | ~23 MB | ~23 MB | ~23 MB โ ๏ธ |
| NativeAOT (native binary) | 2.2 MB | 2.2 MB | 2.2 MB โ ๏ธ |
โ ๏ธ = binary is produced but crashes at runtime.
๐ What this meansโ
- โ Framework-dependent: GenDI adds 28 KB (the library DLL + PDB + XML docs). Irrelevant for any real deployment. Reflection scanner has no overhead here.
- โ Self-contained: The .NET runtime bundle (~80 MB) eclipses everything. All three strategies produce identical output sizes.
- โ
Trimmed: The IL linker statically analyses the generated factories (no reflection โ full
visibility) and removes all unused GenDI internals. Final size is identical to manual.
โ The reflection scanner triggers IL2026 / IL2072 trimmer warnings โ the implementation types get stripped and the binary crashes at startup. - โ
NativeAOT: GenDI generates zero-reflection factory code. The AOT compiler produces an
identical 2.2 MB native binary to hand-written registration.
โ The reflection scanner generates the same compiler warnings and the native binary crashes at startup โAssembly.GetTypes()is incompatible with AOT.
๐ฏ GenDI adds 28 KB in framework-dependent mode only. Under trimming or NativeAOT the final binary is equivalent to writing every
Add*<>()call by hand โ and actually works. The reflection scanner produces equally-sized binaries but they crash at runtime in both trimming and AOT scenarios.
To reproduce locally:
# Framework-dependent
dotnet publish MyApp.csproj -c Release -o out-fd
# Self-contained
dotnet publish MyApp.csproj -c Release -r linux-x64 --self-contained -o out-sc
# Trimmed
dotnet publish MyApp.csproj -c Release -r linux-x64 --self-contained /p:PublishTrimmed=true -o out-trimmed
# NativeAOT
dotnet publish MyApp.csproj -c Release -r linux-x64 /p:PublishAot=true -o out-aot
๐ Published benchmark reportsโ
Full benchmark reports are published in the repository at:
- ๐
docs/BENCHMARKS.md