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 | 27.2 µs ±714 ns | 29.2 µs ±546 ns | 1.00× | 234.4 KB | +8% |
| Activator_createinstance | 47.5 µs ±354 ns | 39.3 µs ±464 ns | 1.35× | 234.4 KB | -17% |
| Compiled_expression | 33.0 µs ±590 ns | 31.6 µs ±629 ns | 1.08× | 234.4 KB | -4% |
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.1× faster than the baseline.
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| Transient_resolve baseline | 68.9 µs ±595 ns | 57.4 µs ±414 ns | 1.00× | 234.4 KB | -17% |
| Scope_per_resolve | 391.7 µs ±5.1 µs | 353.0 µs ±2.4 µs | 6.15× | 1.45 MB | -10% |
| Singleton_resolve | 37.8 µs ±529 ns | 27.1 µs ±14 ns | 0.47× | — | -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.