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.

๐ŸŽฏ 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>() }
4Reflection scanner (worst case)๐Ÿข Assembly.GetTypes() scan at startupContainer expression-tree compilation

๐Ÿงช 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โ€‹

MethodMeanAllocated
โœ๏ธ Manual registration (no GenDI)1.842 ฮผs5.21 KB
โšก GenDI: constructor injection (generated)2.007 ฮผs5.68 KB
๐Ÿ† GenDI: property injection (generated)2.031 ฮผs5.71 KB
๐Ÿข Reflection registration (no GenDI, assembly scan)37.901 ฮผs14.54 KB

๐Ÿ” 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. Manual registration is marginally faster (~8 %) because it inlines the registration calls directly, while GenDI bundles them inside a generated extension method. This is a constant, one-time startup cost โ€” it has no effect on per-request resolution speed.

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 generatedManual (barely)~8 %Negligible; GenDI saves hours of 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