BENCHMARKS · REFLECTION & DI

Creating objects and resolving services — the metaprogramming taxes. Suite and raw reports: github.com/gdhami-net/dotnet-benchmarks.

Intel Core Ultra 9 285HX · Windows 11 · BenchmarkDotNet 0.15.8 · SDK 10.0.201 · 2026-08-22 · single run — not yet median-of-N · bars/tables: mean time, lower is better

CreateInstances

Create 10,000 instances: direct new vs Activator.CreateInstance vs a compiled expression.

TAKEAWAY If a hot path creates objects from a Type, compile the constructor once (or use a source generator) — Activator per call is the tax you keep paying. And when the type is known at compile time, nothing beats plain new.
In this run: Direct_new is fastest (it is the baseline).
RESULTS
methodnet9.0net10.0ratioallocatedΔ net10.0
Direct_new baseline26.7 µs ±2.9 µs26.4 µs ±1.9 µs1.00×234.4 KB-1%
Activator_createinstance44.7 µs ±1.1 µs35.2 µs ±364 ns1.34×234.4 KB-21%
Compiled_expression26.7 µs ±442 ns28.6 µs ±1.9 µs1.09×234.4 KB+7%
RATIO VS BASELINE · net10.0
Direct_new
1.00×
Activator_createinstance
1.34×
Compiled_expression
1.09×
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)

The ledger starts here — 3 run recorded. Trend lines appear from the second release onward.

THE CODE BEING MEASURED
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.

TAKEAWAY Container resolution is cheap but never free — and creating a scope per resolve is the expensive part, not the lookup. Resolve once per scope, not per call, and let singletons be singletons.
In this run: Singleton_resolve is fastest — 2.0× faster than the baseline.
RESULTS
methodnet9.0net10.0ratioallocatedΔ net10.0
Transient_resolve baseline66.7 µs ±981 ns54.0 µs ±4.4 µs1.00×234.4 KB-19%
Scope_per_resolve399.7 µs ±26.3 µs351.8 µs ±10.5 µs6.52×1.45 MB-12%
Singleton_resolve37.4 µs ±58 ns27.0 µs ±17 ns0.50×-28%
RATIO VS BASELINE · net10.0
Transient_resolve
1.00×
Scope_per_resolve
6.52×
Singleton_resolve
0.50×
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)

The ledger starts here — 3 run recorded. Trend lines appear from the second release onward.

THE CODE BEING MEASURED
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.

Direct_newActivator_createinstanceCompiled_expressionTransient_resolveScope_per_resolveSingleton_resolve
mean time → (log)allocated → (log)Direct_newActivator_createinstanceCompiled_expressionTransient_resolveScope_per_resolveSingleton_resolve