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-09-01 · median of 4 quiet runs · typical spread ±3.3% · 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 baseline27.2 µs ±714 ns29.2 µs ±546 ns1.00×234.4 KB+8%
Activator_createinstance47.5 µs ±354 ns39.3 µs ±464 ns1.35×234.4 KB-17%
Compiled_expression33.0 µs ±590 ns31.6 µs ±629 ns1.08×234.4 KB-4%
RATIO VS BASELINE · net10.0
Direct_new1.00×
Activator_createinstance1.35×
Compiled_expression1.08×
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.1× faster than the baseline.
RESULTS
methodnet9.0net10.0ratioallocatedΔ net10.0
Transient_resolve baseline68.9 µs ±595 ns57.4 µs ±414 ns1.00×234.4 KB-17%
Scope_per_resolve391.7 µs ±5.1 µs353.0 µs ±2.4 µs6.15×1.45 MB-10%
Singleton_resolve37.8 µs ±529 ns27.1 µs ±14 ns0.47×—-28%
RATIO VS BASELINE · net10.0
Transient_resolve1.00×
Scope_per_resolve6.15×
Singleton_resolve0.47×
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