Skip to main content

๐Ÿ“Š 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โ€‹

#DescriptionHow registration happensHow activation happens
1Manual (no GenDI)โœ๏ธ Hand-written AddSingleton<> / AddTransient<>Container expression-tree compilation (one-time reflection)
2GenDI โ€” constructor injectionโšก AddGenDIServices() (compile-time generated)Generated factory: new Service(sp.Get<A>(), sp.Get<B>())
3GenDI โ€” property injectionโšก AddGenDIServices() (compile-time generated)Generated factory: new Service { A = sp.Get<A>(), B = sp.Get<B>() }
4GenDI: property with decorator 1โšก AddGenDIServices() (compile-time generated)Generated factory: new Decorator { Inner = new Service { A = sp.Get<A>(), B = sp.Get<B>() } }
5Reflection scanner (worst case)๐Ÿข Assembly.GetTypes() scan at startupContainer 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

MethodMeanAllocated
Manual registration (no GenDI)3.363 ฮผs7.42 KB
GenDI: constructor injection (generated)4.689 ฮผs9.98 KB
GenDI: property injection (generated)4.594 ฮผs9.98 KB
GenDI: with decorator, property injection (generated)15.408 ฮผs14.3 KB
Reflection registration (no GenDI, assembly scan)77.987 ฮผs23.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๐Ÿ† WinnerMarginTakeaway
โœ๏ธ Manual vs โšก GenDI generatedGenDI (latest CI snapshot)Constructor: ~20.1 %, Property: ~17.7 %Generated registration is currently fastest and removes manual maintenance
โšก Constructor vs ๐Ÿ† property injectionTieยฑ1โ€“2 % (noise)Use property injection โ€” zero cost, big ergonomic win
โšก GenDI generated vs ๐Ÿข reflection scannerGenDI~19ร— fasterReflection 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 KB292 KB264 KB
Framework-dependent (app DLL)8 KB8 KB8 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 MB2.2 MB2.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