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.
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.
In this run: FrozenDictionary_lookup is fastest — 1.3× faster than the baseline.
RESULTS — 1000
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| Dictionary_lookup baseline | 6.3 µs ±41 ns | 3.8 µs ±6 ns | 1.00× | — | -40% |
| FrozenDictionary_lookup | 3.1 µs ±29 ns | 3.0 µs ±14 ns | 0.80× | — | -2% |
| ImmutableDictionary_lookup | 9.1 µs ±215 ns | 9.5 µs ±1.5 µs | 2.52× | — | +5% |
| SortedArray_binarysearch | 47.3 µs ±2.1 µs | 37.7 µs ±1.8 µs | 9.98× | — | -20% |
RATIO VS BASELINE — 1000 · net10.0
RESULTS — 100000
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| Dictionary_lookup baseline | 9.5 µs ±120 ns | 6.6 µs ±51 ns | 1.00× | — | -30% |
| FrozenDictionary_lookup | 5.8 µs ±96 ns | 5.6 µs ±37 ns | 0.85× | — | -2% |
| ImmutableDictionary_lookup | 52.0 µs ±6.2 µs | 72.5 µs ±3.1 µs | 10.95× | — | +39% |
| SortedArray_binarysearch | 109.9 µs ±1.8 µs | 87.9 µs ±557 ns | 13.29× | — | -20% |
RATIO VS BASELINE — 100000 · net10.0
SCALING · net10.0 · MEAN TIME BY INPUT SIZE (LOG)
Dictionary_lookupFrozenDictionary_lookupImmutableDictionary_lookupSortedArray_binarysearch
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
Dictionary_lookupFrozenDictionary_lookupImmutableDictionary_lookupSortedArray_binarysearch
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.
In this run: FrozenDictionary_miss is fastest — 4.7× faster than the baseline.
RESULTS — 1000
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| Dictionary_miss baseline | 4.1 µs ±30 ns | 3.2 µs ±3 ns | 1.00× | — | -21% |
| FrozenDictionary_miss | 620 ns ±0 ns | 682 ns ±0 ns | 0.21× | — | +10% |
| ImmutableDictionary_miss | 8.8 µs ±37 ns | 10.8 µs ±675 ns | 3.34× | — | +23% |
| SortedArray_miss | 14.0 µs ±180 ns | 7.7 µs ±129 ns | 2.39× | — | -45% |
RATIO VS BASELINE — 1000 · net10.0
RESULTS — 100000
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| Dictionary_miss baseline | 5.1 µs ±47 ns | 4.2 µs ±4 ns | 1.00× | — | -18% |
| FrozenDictionary_miss | 620 ns ±0 ns | 688 ns ±1 ns | 0.16× | — | +11% |
| ImmutableDictionary_miss | 57.4 µs ±10.7 µs | 76.4 µs ±9.2 µs | 18.27× | — | +33% |
| SortedArray_miss | 22.3 µs ±263 ns | 20.4 µs ±62 ns | 4.87× | — | -9% |
RATIO VS BASELINE — 100000 · net10.0
SCALING · net10.0 · MEAN TIME BY INPUT SIZE (LOG)
Dictionary_missFrozenDictionary_missImmutableDictionary_missSortedArray_miss
THE LEDGER · SAME WORKLOAD ACROSS RELEASES (LOG)
Dictionary_missFrozenDictionary_missImmutableDictionary_missSortedArray_miss
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;
}