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-08-22 · single run — not yet median-of-N · 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 ±23 ns3.9 µs ±113 ns1.00×-38%
FrozenDictionary_lookup3.1 µs ±28 ns3.0 µs ±13 ns0.78×-2%
ImmutableDictionary_lookup9.9 µs ±1.4 µs8.1 µs ±101 ns2.11×-18%
SortedArray_binarysearch47.3 µs ±3.6 µs38.3 µs ±3.2 µs9.91×-19%
RATIO VS BASELINE — 1000 · net10.0
Dictionary_lookup
1.00×
FrozenDictionary_lookup
0.78×
ImmutableDictionary_lookup
2.11×
SortedArray_binarysearch
9.91×
RESULTS — 100000
methodnet9.0net10.0ratioallocatedΔ net10.0
Dictionary_lookup baseline9.5 µs ±107 ns6.6 µs ±57 ns1.00×-31%
FrozenDictionary_lookup5.8 µs ±114 ns5.6 µs ±34 ns0.85×-2%
ImmutableDictionary_lookup56.9 µs ±11.4 µs72.3 µs ±5.5 µs10.95×+27%
SortedArray_binarysearch108.6 µs ±2.9 µs88.3 µs ±2.4 µs13.38×-19%
RATIO VS BASELINE — 100000 · net10.0
Dictionary_lookup
1.00×
FrozenDictionary_lookup
0.85×
ImmutableDictionary_lookup
10.95×
SortedArray_binarysearch
13.38×
SCALING · net10.0 · MEAN TIME BY INPUT SIZE (LOG)
Dictionary_lookupFrozenDictionary_lookupImmutableDictionary_lookupSortedArray_binarysearch
99.1 µ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 · 08-22 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.6 µs ±8 ns3.2 µs ±12 ns1.00×-29%
FrozenDictionary_miss620 ns ±0 ns685 ns ±1 ns0.21×+11%
ImmutableDictionary_miss9.3 µs ±833 ns11.3 µs ±2.0 µs3.48×+21%
SortedArray_miss13.9 µs ±101 ns7.7 µs ±176 ns2.39×-44%
RATIO VS BASELINE — 1000 · net10.0
Dictionary_miss
1.00×
FrozenDictionary_miss
0.21×
ImmutableDictionary_miss
3.48×
SortedArray_miss
2.39×
RESULTS — 100000
methodnet9.0net10.0ratioallocatedΔ net10.0
Dictionary_miss baseline5.2 µs ±54 ns4.2 µs ±5 ns1.00×-20%
FrozenDictionary_miss626 ns ±9 ns689 ns ±5 ns0.16×+10%
ImmutableDictionary_miss56.6 µs ±12.3 µs75.8 µs ±9.5 µs18.16×+34%
SortedArray_miss22.1 µs ±103 ns21.0 µs ±81 ns5.02×-5%
RATIO VS BASELINE — 100000 · net10.0
Dictionary_miss
1.00×
FrozenDictionary_miss
0.16×
ImmutableDictionary_miss
18.16×
SortedArray_miss
5.02×
SCALING · net10.0 · MEAN TIME BY INPUT SIZE (LOG)
Dictionary_missFrozenDictionary_missImmutableDictionary_missSortedArray_miss
85.1 µs7.2 µs611 ns1000100000
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
Dictionary_missFrozenDictionary_missImmutableDictionary_missSortedArray_miss
17.1 µs3.2 µs611 ns10.0.201 · 08-22 v210.0.201 · 08-22 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;
}