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 ±23 ns | 3.9 µs ±113 ns | 1.00× | — | -38% |
| FrozenDictionary_lookup | 3.1 µs ±28 ns | 3.0 µs ±13 ns | 0.78× | — | -2% |
| ImmutableDictionary_lookup | 9.9 µs ±1.4 µs | 8.1 µs ±101 ns | 2.11× | — | -18% |
| SortedArray_binarysearch | 47.3 µs ±3.6 µs | 38.3 µs ±3.2 µs | 9.91× | — | -19% |
RATIO VS BASELINE — 1000 · net10.0
RESULTS — 100000
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| Dictionary_lookup baseline | 9.5 µs ±107 ns | 6.6 µs ±57 ns | 1.00× | — | -31% |
| FrozenDictionary_lookup | 5.8 µs ±114 ns | 5.6 µs ±34 ns | 0.85× | — | -2% |
| ImmutableDictionary_lookup | 56.9 µs ±11.4 µs | 72.3 µs ±5.5 µs | 10.95× | — | +27% |
| SortedArray_binarysearch | 108.6 µs ±2.9 µs | 88.3 µs ±2.4 µs | 13.38× | — | -19% |
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.6 µs ±8 ns | 3.2 µs ±12 ns | 1.00× | — | -29% |
| FrozenDictionary_miss | 620 ns ±0 ns | 685 ns ±1 ns | 0.21× | — | +11% |
| ImmutableDictionary_miss | 9.3 µs ±833 ns | 11.3 µs ±2.0 µs | 3.48× | — | +21% |
| SortedArray_miss | 13.9 µs ±101 ns | 7.7 µs ±176 ns | 2.39× | — | -44% |
RATIO VS BASELINE — 1000 · net10.0
RESULTS — 100000
| method | net9.0 | net10.0 | ratio | allocated | Δ net10.0 |
|---|---|---|---|---|---|
| Dictionary_miss baseline | 5.2 µs ±54 ns | 4.2 µs ±5 ns | 1.00× | — | -20% |
| FrozenDictionary_miss | 626 ns ±9 ns | 689 ns ±5 ns | 0.16× | — | +10% |
| ImmutableDictionary_miss | 56.6 µs ±12.3 µs | 75.8 µs ±9.5 µs | 18.16× | — | +34% |
| SortedArray_miss | 22.1 µs ±103 ns | 21.0 µs ±81 ns | 5.02× | — | -5% |
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;
}