Creating objects and resolving services — the metaprogramming taxes. Suite and raw reports: github.com/gdhami-net/dotnet-benchmarks.
CreateInstances
Create 10,000 instances: direct new vs Activator.CreateInstance vs a compiled expression.
In this run: Direct_new is fastest (it is the baseline).
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| Direct_new baseline | 26.7 µs ±2.9 µs | 26.4 µs ±1.9 µs | 1.00× | 234.4 KB | -1% |
| Activator_createinstance | 44.7 µs ±1.1 µs | 35.2 µs ±364 ns | 1.34× | 234.4 KB | -21% |
| Compiled_expression | 26.7 µs ±442 ns | 28.6 µs ±1.9 µs | 1.09× | 234.4 KB | +7% |
The ledger starts here — 3 run recorded. Trend lines appear from the second release onward.
Direct_new — what this measures
Plain new — the compiler knows everything.
[Benchmark (Baseline)]
public int Direct_new()
{
var sum = 0;
for (var i = 0; i < 10_000; i++) sum += new Widget().Value;
return sum;
}Activator_createinstance — what this measures
Activator.CreateInstance from a Type — reflection every call.
[Benchmark]
public int Activator_createinstance()
{
var sum = 0;
for (var i = 0; i < 10_000; i++) sum += ((Widget)Activator.CreateInstance(typeof(Widget))!).Value;
return sum;
}Compiled_expression — what this measures
An Expression-compiled constructor delegate — reflection once, then raw speed.
[Benchmark]
public int Compiled_expression()
{
var sum = 0;
for (var i = 0; i < 10_000; i++) sum += CompiledCtor().Value;
return sum;
}ResolveServices
Resolve 10,000 services from Microsoft.Extensions.DependencyInjection: transient vs scoped-per-resolve vs singleton.
In this run: Singleton_resolve is fastest — 2.0× faster than the baseline.
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| Transient_resolve baseline | 66.7 µs ±981 ns | 54.0 µs ±4.4 µs | 1.00× | 234.4 KB | -19% |
| Scope_per_resolve | 399.7 µs ±26.3 µs | 351.8 µs ±10.5 µs | 6.52× | 1.45 MB | -12% |
| Singleton_resolve | 37.4 µs ±58 ns | 27.0 µs ±17 ns | 0.50× | — | -28% |
The ledger starts here — 3 run recorded. Trend lines appear from the second release onward.
Transient_resolve — what this measures
Transient resolve straight from the root provider.
[Benchmark (Baseline)]
public int Transient_resolve()
{
var sum = 0;
for (var i = 0; i < 10_000; i++) sum += _provider.GetRequiredService<Widget>().Value;
return sum;
}Scope_per_resolve — what this measures
A new scope per resolve — the pattern that sneaks into middleware.
[Benchmark]
public int Scope_per_resolve()
{
var sum = 0;
for (var i = 0; i < 10_000; i++)
{
using var scope = _provider.CreateScope();
sum += scope.ServiceProvider.GetRequiredService<Widget>().Value;
}
return sum;
}Singleton_resolve — what this measures
Singleton resolve — a dictionary lookup after the first call.
[Benchmark]
public int Singleton_resolve()
{
var sum = 0;
for (var i = 0; i < 10_000; i++) sum += _provider.GetRequiredService<List<Widget>>().Count;
return sum;
}speed vs allocation
Fast is one axis. What it costs the GC is the other.