BENCHMARKS · COLLECTIONS

Four lookup structures, hit and miss — including the case where every key is absent. 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

LookupHit

Look up 1,000 keys that EXIST: Dictionary vs FrozenDictionary vs ImmutableDictionary vs binary search over a sorted array.

TAKEAWAY Build-once, read-forever dictionaries should be FrozenDictionary. Never use ImmutableDictionary for read performance - it's a versioning structure, not a fast map.
In this run: FrozenDictionary_lookup is fastest — 1.3× faster than the baseline.
RESULTS — 1000
methodnet9.0net10.0ratioallocatedΔ net10.0
Dictionary_lookup baseline6.3 µs ±41 ns3.8 µs ±6 ns1.00×—-40%
FrozenDictionary_lookup3.1 µs ±29 ns3.0 µs ±14 ns0.80×—-2%
ImmutableDictionary_lookup9.1 µs ±215 ns9.5 µs ±1.5 µs2.52×—+5%
SortedArray_binarysearch47.3 µs ±2.1 µs37.7 µs ±1.8 µs9.98×—-20%
RATIO VS BASELINE — 1000 · net10.0
Dictionary_lookup1.00×
FrozenDictionary_lookup0.80×
ImmutableDictionary_lookup2.52×
SortedArray_binarysearch9.98×
RESULTS — 100000
methodnet9.0net10.0ratioallocatedΔ net10.0
Dictionary_lookup baseline9.5 µs ±120 ns6.6 µs ±51 ns1.00×—-30%
FrozenDictionary_lookup5.8 µs ±96 ns5.6 µs ±37 ns0.85×—-2%
ImmutableDictionary_lookup52.0 µs ±6.2 µs72.5 µs ±3.1 µs10.95×—+39%
SortedArray_binarysearch109.9 µs ±1.8 µs87.9 µs ±557 ns13.29×—-20%
RATIO VS BASELINE — 100000 · net10.0
Dictionary_lookup1.00×
FrozenDictionary_lookup0.85×
ImmutableDictionary_lookup10.95×
SortedArray_binarysearch13.29×
SCALING · net10.0 · MEAN TIME BY INPUT SIZE (LOG)
Dictionary_lookupFrozenDictionary_lookupImmutableDictionary_lookupSortedArray_binarysearch
98.6 µs16.3 µs2.7 µs1000100000
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
Dictionary_lookupFrozenDictionary_lookupImmutableDictionary_lookupSortedArray_binarysearch
46.2 µs11.1 µs2.7 µs10.0.201 · 08-22 v210.0.201 · 09-01 v3
THE CODE BEING MEASURED
Dictionary_lookup — what this measures

Plain Dictionary — the default.

[Benchmark (Baseline)]
public int Dictionary_lookup()
{
    var hits = 0;
    foreach (var p in _probes)
        if (_dict.TryGetValue(p, out _)) hits++;
    return hits;
}
FrozenDictionary_lookup — what this measures

FrozenDictionary (.NET 8+): built once, optimized for reads.

[Benchmark]
public int FrozenDictionary_lookup()
{
    var hits = 0;
    foreach (var p in _probes)
        if (_frozen.TryGetValue(p, out _)) hits++;
    return hits;
}
ImmutableDictionary_lookup — what this measures

ImmutableDictionary — immutability paid for with a tree, not a table.

[Benchmark]
public int ImmutableDictionary_lookup()
{
    var hits = 0;
    foreach (var p in _probes)
        if (_immutable.TryGetValue(p, out _)) hits++;
    return hits;
}
SortedArray_binarysearch — what this measures

Array.BinarySearch over sorted keys — O(log n) without a hash table.

[Benchmark]
public int SortedArray_binarysearch()
{
    var hits = 0;
    foreach (var p in _probes)
        if (Array.BinarySearch(_sortedKeys, p, StringComparer.Ordinal) >= 0) hits++;
    return hits;
}

LookupMiss

The "what if" twin: the same four lookups when every key is ABSENT — miss cost is what degrades under attack or bad data.

TAKEAWAY Check the miss cost, not just the hit cost - bad input and attacks are all misses. The ranking holds: frozen fastest, immutable slowest.
In this run: FrozenDictionary_miss is fastest — 4.7× faster than the baseline.
RESULTS — 1000
methodnet9.0net10.0ratioallocatedΔ net10.0
Dictionary_miss baseline4.1 µs ±30 ns3.2 µs ±3 ns1.00×—-21%
FrozenDictionary_miss620 ns ±0 ns682 ns ±0 ns0.21×—+10%
ImmutableDictionary_miss8.8 µs ±37 ns10.8 µs ±675 ns3.34×—+23%
SortedArray_miss14.0 µs ±180 ns7.7 µs ±129 ns2.39×—-45%
RATIO VS BASELINE — 1000 · net10.0
Dictionary_miss1.00×
FrozenDictionary_miss0.21×
ImmutableDictionary_miss3.34×
SortedArray_miss2.39×
RESULTS — 100000
methodnet9.0net10.0ratioallocatedΔ net10.0
Dictionary_miss baseline5.1 µs ±47 ns4.2 µs ±4 ns1.00×—-18%
FrozenDictionary_miss620 ns ±0 ns688 ns ±1 ns0.16×—+11%
ImmutableDictionary_miss57.4 µs ±10.7 µs76.4 µs ±9.2 µs18.27×—+33%
SortedArray_miss22.3 µs ±263 ns20.4 µs ±62 ns4.87×—-9%
RATIO VS BASELINE — 100000 · net10.0
Dictionary_miss1.00×
FrozenDictionary_miss0.16×
ImmutableDictionary_miss18.27×
SortedArray_miss4.87×
SCALING · net10.0 · MEAN TIME BY INPUT SIZE (LOG)
Dictionary_missFrozenDictionary_missImmutableDictionary_missSortedArray_miss
85.7 µs7.2 µs608 ns1000100000
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
Dictionary_missFrozenDictionary_missImmutableDictionary_missSortedArray_miss
17.1 µs3.2 µs608 ns10.0.201 · 08-22 v210.0.201 · 09-01 v3
THE CODE BEING MEASURED
Dictionary_miss — what this measures

Dictionary, all misses.

[Benchmark (Baseline)]
public int Dictionary_miss()
{
    var hits = 0;
    foreach (var p in _probes)
        if (_dict.TryGetValue(p, out _)) hits++;
    return hits;
}
FrozenDictionary_miss — what this measures

FrozenDictionary, all misses.

[Benchmark]
public int FrozenDictionary_miss()
{
    var hits = 0;
    foreach (var p in _probes)
        if (_frozen.TryGetValue(p, out _)) hits++;
    return hits;
}
ImmutableDictionary_miss — what this measures

ImmutableDictionary, all misses.

[Benchmark]
public int ImmutableDictionary_miss()
{
    var hits = 0;
    foreach (var p in _probes)
        if (_immutable.TryGetValue(p, out _)) hits++;
    return hits;
}
SortedArray_miss — what this measures

Sorted-array binary search, all misses.

[Benchmark]
public int SortedArray_miss()
{
    var hits = 0;
    foreach (var p in _probes)
        if (Array.BinarySearch(_sortedKeys, p, StringComparer.Ordinal) >= 0) hits++;
    return hits;
}