Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save udlose/1b1e8947ba53bfbb595550275a4efaef to your computer and use it in GitHub Desktop.

Select an option

Save udlose/1b1e8947ba53bfbb595550275a4efaef to your computer and use it in GitHub Desktop.
Dictionary Lookup Hit vs Miss for Guid vs string
[MemoryDiagnoser]
public class DictionaryKeyLookupHitBenchmarks
{
private readonly Consumer _consumer = new Consumer();
private Guid[] _guidKeys = Array.Empty<Guid>();
private string[] _string20Keys = Array.Empty<string>();
private string[] _string50Keys = Array.Empty<string>();
private Dictionary<Guid, int> _guidDictionary = new Dictionary<Guid, int>();
private Dictionary<string, int> _string20Dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
private Dictionary<string, int> _string50Dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
private int[] _lookupOrder = Array.Empty<int>();
[Params(500, 1_000, 5_000)]
public int DictionarySize { get; set; }
[GlobalSetup]
public void GlobalSetup()
{
int count = DictionarySize;
_guidKeys = new Guid[count];
for (int i = 0; i < count; i++)
{
_guidKeys[i] = Guid.NewGuid();
}
_string20Keys = CreateUniqueStrings(count, 20, seed: 12345, StringComparer.OrdinalIgnoreCase);
_string50Keys = CreateUniqueStrings(count, 50, seed: 54321, StringComparer.OrdinalIgnoreCase);
_guidDictionary = new Dictionary<Guid, int>(count);
_string20Dictionary = new Dictionary<string, int>(count, StringComparer.OrdinalIgnoreCase);
_string50Dictionary = new Dictionary<string, int>(count, StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < count; i++)
{
_guidDictionary.Add(_guidKeys[i], i);
_string20Dictionary.Add(_string20Keys[i], i);
_string50Dictionary.Add(_string50Keys[i], i);
}
_lookupOrder = CreateShuffledIndices(count, seed: 424242);
}
[Benchmark(Baseline = true)]
[MethodImpl(MethodImplOptions.NoInlining)]
public void TryGetValue_Hit_Guid()
{
// use locals to avoid noise from property reads
int count = _lookupOrder.Length;
Dictionary<Guid, int> dictionary = _guidDictionary;
Guid[] keys = _guidKeys;
int[] order = _lookupOrder;
Consumer consumer = _consumer;
for (int i = 0; i < count; i++)
{
Guid key = keys[order[i]];
bool found = dictionary.TryGetValue(key, out int value);
if (!found)
{
throw new InvalidOperationException("Expected hit but got miss.");
}
consumer.Consume(value);
}
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void TryGetValue_Hit_String_20()
{
// use locals to avoid noise from property reads
int count = _lookupOrder.Length;
Dictionary<string, int> dictionary = _string20Dictionary;
string[] keys = _string20Keys;
int[] order = _lookupOrder;
Consumer consumer = _consumer;
for (int i = 0; i < count; i++)
{
string key = keys[order[i]];
bool found = dictionary.TryGetValue(key, out int value);
if (!found)
{
throw new InvalidOperationException("Expected hit but got miss.");
}
consumer.Consume(value);
}
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void TryGetValue_Hit_String_50()
{
// use locals to avoid noise from property reads
int count = _lookupOrder.Length;
Dictionary<string, int> dictionary = _string50Dictionary;
string[] keys = _string50Keys;
int[] order = _lookupOrder;
Consumer consumer = _consumer;
for (int i = 0; i < count; i++)
{
string key = keys[order[i]];
bool found = dictionary.TryGetValue(key, out int value);
if (!found)
{
throw new InvalidOperationException("Expected hit but got miss.");
}
consumer.Consume(value);
}
}
private static int[] CreateShuffledIndices(int count, int seed)
{
int[] indices = new int[count];
for (int i = 0; i < count; i++)
{
indices[i] = i;
}
Random random = new Random(seed);
for (int i = count - 1; i > 0; i--)
{
int j = random.Next(i + 1);
int tmp = indices[i];
indices[i] = indices[j];
indices[j] = tmp;
}
return indices;
}
private static string[] CreateUniqueStrings(int count, int length, int seed, StringComparer comparer)
{
HashSet<string> seen = new HashSet<string>(comparer);
string[] result = new string[count];
Random random = new Random(seed);
const string alphabet = "abcdefghijklmnopqrstuvwxyz";
int alphabetLength = alphabet.Length;
for (int i = 0; i < count; i++)
{
while (true)
{
char[] chars = new char[length];
for (int c = 0; c < length; c++)
{
chars[c] = alphabet[random.Next(alphabetLength)];
}
string candidate = new string(chars);
if (seen.Add(candidate))
{
result[i] = candidate;
break;
}
}
}
return result;
}
[Config(typeof(DictionaryKeyLookupHitBenchmarksConfig))]
public class DictionaryKeyLookupHitBenchmarksConfig : ManualConfig
{
public DictionaryKeyLookupHitBenchmarksConfig()
{
SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend);
}
}
}
[MemoryDiagnoser]
public class DictionaryKeyLookupMissBenchmarks
{
private readonly Consumer _consumer = new Consumer();
private Guid[] _guidMissKeys = Array.Empty<Guid>();
private string[] _string20MissKeys = Array.Empty<string>();
private string[] _string50MissKeys = Array.Empty<string>();
private Dictionary<Guid, int> _guidDictionary = new Dictionary<Guid, int>();
private Dictionary<string, int> _string20Dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
private Dictionary<string, int> _string50Dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
private int[] _lookupOrder = Array.Empty<int>();
[Params(500, 1_000, 5_000)]
public int DictionarySize { get; set; }
[GlobalSetup]
public void GlobalSetup()
{
int count = DictionarySize;
// Build dictionaries with "present" keys
Guid[] guidPresentKeys = new Guid[count];
for (int i = 0; i < count; i++)
{
guidPresentKeys[i] = Guid.NewGuid();
}
string[] string20PresentKeys = CreateUniqueStrings(count, 20, seed: 12345, StringComparer.OrdinalIgnoreCase);
string[] string50PresentKeys = CreateUniqueStrings(count, 50, seed: 54321, StringComparer.OrdinalIgnoreCase);
_guidDictionary = new Dictionary<Guid, int>(count);
_string20Dictionary = new Dictionary<string, int>(count, StringComparer.OrdinalIgnoreCase);
_string50Dictionary = new Dictionary<string, int>(count, StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < count; i++)
{
_guidDictionary.Add(guidPresentKeys[i], i);
_string20Dictionary.Add(string20PresentKeys[i], i);
_string50Dictionary.Add(string50PresentKeys[i], i);
}
// Generate miss keys (guaranteed absent)
_guidMissKeys = CreateGuidMissKeys(count, guidPresentKeys);
_string20MissKeys = CreateUniqueStringsExcluding(count, 20, seed: 22222, StringComparer.OrdinalIgnoreCase, string20PresentKeys);
_string50MissKeys = CreateUniqueStringsExcluding(count, 50, seed: 33333, StringComparer.OrdinalIgnoreCase, string50PresentKeys);
_lookupOrder = CreateShuffledIndices(count, seed: 424242);
}
[Benchmark(Baseline = true)]
[MethodImpl(MethodImplOptions.NoInlining)]
public void TryGetValue_Miss_Guid()
{
// use locals to avoid noise from property reads
int count = _lookupOrder.Length;
Dictionary<Guid, int> dictionary = _guidDictionary;
Guid[] keys = _guidMissKeys;
int[] order = _lookupOrder;
Consumer consumer = _consumer;
for (int i = 0; i < count; i++)
{
Guid key = keys[order[i]];
bool found = dictionary.TryGetValue(key, out int value);
if (found)
{
throw new InvalidOperationException("Expected miss but got hit.");
}
consumer.Consume(found);
consumer.Consume(value);
}
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void TryGetValue_Miss_String_20()
{
// use locals to avoid noise from property reads
int count = _lookupOrder.Length;
Dictionary<string, int> dictionary = _string20Dictionary;
string[] keys = _string20MissKeys;
int[] order = _lookupOrder;
Consumer consumer = _consumer;
for (int i = 0; i < count; i++)
{
string key = keys[order[i]];
bool found = dictionary.TryGetValue(key, out int value);
if (found)
{
throw new InvalidOperationException("Expected miss but got hit.");
}
consumer.Consume(found);
consumer.Consume(value);
}
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void TryGetValue_Miss_String_50()
{
// use locals to avoid noise from property reads
int count = _lookupOrder.Length;
Dictionary<string, int> dictionary = _string50Dictionary;
string[] keys = _string50MissKeys;
int[] order = _lookupOrder;
Consumer consumer = _consumer;
for (int i = 0; i < count; i++)
{
string key = keys[order[i]];
bool found = dictionary.TryGetValue(key, out int value);
if (found)
{
throw new InvalidOperationException("Expected miss but got hit.");
}
consumer.Consume(found);
consumer.Consume(value);
}
}
private static Guid[] CreateGuidMissKeys(int count, Guid[] presentKeys)
{
HashSet<Guid> present = new HashSet<Guid>(presentKeys);
Guid[] result = new Guid[count];
for (int i = 0; i < count; i++)
{
while (true)
{
Guid candidate = Guid.NewGuid();
if (!present.Contains(candidate))
{
result[i] = candidate;
break;
}
}
}
return result;
}
private static int[] CreateShuffledIndices(int count, int seed)
{
int[] indices = new int[count];
for (int i = 0; i < count; i++)
{
indices[i] = i;
}
Random random = new Random(seed);
for (int i = count - 1; i > 0; i--)
{
int j = random.Next(i + 1);
int tmp = indices[i];
indices[i] = indices[j];
indices[j] = tmp;
}
return indices;
}
private static string[] CreateUniqueStrings(int count, int length, int seed, StringComparer comparer)
{
HashSet<string> seen = new HashSet<string>(comparer);
string[] result = new string[count];
Random random = new Random(seed);
const string alphabet = "abcdefghijklmnopqrstuvwxyz";
int alphabetLength = alphabet.Length;
for (int i = 0; i < count; i++)
{
while (true)
{
char[] chars = new char[length];
for (int c = 0; c < length; c++)
{
chars[c] = alphabet[random.Next(alphabetLength)];
}
string candidate = new string(chars);
if (seen.Add(candidate))
{
result[i] = candidate;
break;
}
}
}
return result;
}
private static string[] CreateUniqueStringsExcluding(int count, int length, int seed, StringComparer comparer, string[] exclude)
{
HashSet<string> seen = new HashSet<string>(comparer);
for (int i = 0; i < exclude.Length; i++)
{
seen.Add(exclude[i]);
}
string[] result = new string[count];
Random random = new Random(seed);
const string alphabet = "abcdefghijklmnopqrstuvwxyz";
int alphabetLength = alphabet.Length;
for (int i = 0; i < count; i++)
{
while (true)
{
char[] chars = new char[length];
for (int c = 0; c < length; c++)
{
chars[c] = alphabet[random.Next(alphabetLength)];
}
string candidate = new string(chars);
if (seen.Add(candidate))
{
result[i] = candidate;
break;
}
}
}
return result;
}
[Config(typeof(DictionaryKeyLookupMissBenchmarksConfig))]
public class DictionaryKeyLookupMissBenchmarksConfig : ManualConfig
{
public DictionaryKeyLookupMissBenchmarksConfig()
{
SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend);
}
}
}
public static class Program
{
public static void Main(string[] args)
{
BenchmarkSwitcher
.FromTypes(new Type[]
{
typeof(DictionaryKeyLookupHitBenchmarks),
typeof(DictionaryKeyLookupMissBenchmarks)
})
.Run(args);
}
}
/************** DICTIONARY LOOKUP (HIT) BENCHMARK SUMMARY ******************/
//
// Run time: 00:06:06 (366.54 sec), executed benchmarks: 9
//
// * Summary *
//
// BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7628/25H2/2025Update/HudsonValley2)
// AMD Ryzen 9 7945HX with Radeon Graphics 2.50GHz, 1 CPU, 32 logical and 16 physical cores
//
// .NET SDK 10.0.102
// [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v4
//
//| Method | DictionarySize | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio |
//|-------------------------- |--------------- |-----------:|----------:|----------:|-----------:|------:|--------:|----------:|------------:|
//| TryGetValue_Hit_Guid | 500 | 1.854 μs | 0.0365 μs | 0.0648 μs | 1.888 μs | 1.00 | 0.05 | - | NA |
//| TryGetValue_Hit_String_20 | 500 | 7.187 μs | 0.1429 μs | 0.2504 μs | 7.193 μs | 3.88 | 0.19 | - | NA |
//| TryGetValue_Hit_String_50 | 500 | 14.075 μs | 0.2381 μs | 0.2228 μs | 14.056 μs | 7.60 | 0.29 | - | NA |
//| | | | | | | | | | |
//| TryGetValue_Hit_Guid | 1000 | 4.770 μs | 0.1329 μs | 0.3854 μs | 4.712 μs | 1.01 | 0.11 | - | NA |
//| TryGetValue_Hit_String_20 | 1000 | 15.683 μs | 0.3113 μs | 0.5922 μs | 15.576 μs | 3.31 | 0.28 | - | NA |
//| TryGetValue_Hit_String_50 | 1000 | 30.060 μs | 0.5692 μs | 0.5590 μs | 30.011 μs | 6.34 | 0.50 | - | NA |
//| | | | | | | | | | |
//| TryGetValue_Hit_Guid | 5000 | 27.297 μs | 0.5453 μs | 1.5646 μs | 27.161 μs | 1.00 | 0.08 | - | NA |
//| TryGetValue_Hit_String_20 | 5000 | 92.691 μs | 1.8503 μs | 4.2514 μs | 91.454 μs | 3.41 | 0.25 | 1 B | NA |
//| TryGetValue_Hit_String_50 | 5000 | 179.448 μs | 3.5374 μs | 4.0737 μs | 180.216 μs | 6.60 | 0.40 | 1 B | NA |
/************** DICTIONARY LOOKUP (HIT) BENCHMARK SUMMARY ******************/
//
// Run time: 00:08:07 (487.97 sec), executed benchmarks: 9
//
// * Summary *
//
// BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7628/25H2/2025Update/HudsonValley2)
// AMD Ryzen 9 7945HX with Radeon Graphics 2.50GHz, 1 CPU, 32 logical and 16 physical cores
//
// [Host] : .NET Framework 4.8.1 (4.8.9221.0), X86 LegacyJIT
// DefaultJob : .NET Framework 4.8.1 (4.8.9221.0), X86 LegacyJIT
//
//| Method | DictionarySize | Mean | Error | StdDev | Median | Ratio | RatioSD |
//|-------------------------- |--------------- |----------:|---------:|----------:|----------:|------:|--------:|
//| TryGetValue_Hit_Guid | 500 | 10.84 μs | 0.215 μs | 0.520 μs | 10.84 μs | 1.00 | 0.07 |
//| TryGetValue_Hit_String_20 | 500 | 17.24 μs | 0.506 μs | 1.477 μs | 16.67 μs | 1.59 | 0.16 |
//| TryGetValue_Hit_String_50 | 500 | 34.12 μs | 0.679 μs | 1.547 μs | 33.86 μs | 3.16 | 0.21 |
//| | | | | | | | |
//| TryGetValue_Hit_Guid | 1000 | 19.81 μs | 0.389 μs | 0.382 μs | 19.84 μs | 1.00 | 0.03 |
//| TryGetValue_Hit_String_20 | 1000 | 32.92 μs | 0.518 μs | 1.071 μs | 32.80 μs | 1.66 | 0.06 |
//| TryGetValue_Hit_String_50 | 1000 | 64.96 μs | 1.299 μs | 3.444 μs | 64.20 μs | 3.28 | 0.18 |
//| | | | | | | | |
//| TryGetValue_Hit_Guid | 5000 | 111.66 μs | 2.485 μs | 7.210 μs | 110.71 μs | 1.00 | 0.09 |
//| TryGetValue_Hit_String_20 | 5000 | 204.45 μs | 6.438 μs | 18.881 μs | 201.47 μs | 1.84 | 0.20 |
//| TryGetValue_Hit_String_50 | 5000 | 345.34 μs | 6.840 μs | 19.067 μs | 345.95 μs | 3.11 | 0.26 |
/************** DICTIONARY LOOKUP (MISS) BENCHMARK SUMMARY ******************/
//
// Run time: 00:06:20 (380.35 sec), executed benchmarks: 9
//
// * Summary *
//
// BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7628/25H2/2025Update/HudsonValley2)
// AMD Ryzen 9 7945HX with Radeon Graphics 2.50GHz, 1 CPU, 32 logical and 16 physical cores
//
// .NET SDK 10.0.102
// [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v4
//
//| Method | DictionarySize | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
//|--------------------------- |--------------- |-----------:|----------:|----------:|------:|--------:|----------:|------------:|
//| TryGetValue_Miss_Guid | 500 | 1.862 μs | 0.0370 μs | 0.0576 μs | 1.00 | 0.04 | - | NA |
//| TryGetValue_Miss_String_20 | 500 | 6.772 μs | 0.1328 μs | 0.1817 μs | 3.64 | 0.15 | - | NA |
//| TryGetValue_Miss_String_50 | 500 | 13.071 μs | 0.2119 μs | 0.1982 μs | 7.03 | 0.24 | - | NA |
//| | | | | | | | | |
//| TryGetValue_Miss_Guid | 1000 | 3.931 μs | 0.0795 μs | 0.2331 μs | 1.00 | 0.08 | - | NA |
//| TryGetValue_Miss_String_20 | 1000 | 14.835 μs | 0.2940 μs | 0.5871 μs | 3.79 | 0.27 | - | NA |
//| TryGetValue_Miss_String_50 | 1000 | 27.673 μs | 0.5371 μs | 0.6185 μs | 7.06 | 0.44 | - | NA |
//| | | | | | | | | |
//| TryGetValue_Miss_Guid | 5000 | 22.033 μs | 0.5433 μs | 1.5588 μs | 1.00 | 0.10 | - | NA |
//| TryGetValue_Miss_String_20 | 5000 | 81.441 μs | 1.9391 μs | 5.6565 μs | 3.71 | 0.37 | 1 B | NA |
//| TryGetValue_Miss_String_50 | 5000 | 158.429 μs | 3.1524 μs | 6.1485 μs | 7.23 | 0.58 | 1 B | NA |
/************** DICTIONARY LOOKUP (MISS) BENCHMARK SUMMARY ******************/
//
// Run time: 00:08:39 (519.15 sec), executed benchmarks: 9
//
// * Summary *
//
// BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7628/25H2/2025Update/HudsonValley2)
// AMD Ryzen 9 7945HX with Radeon Graphics 2.50GHz, 1 CPU, 32 logical and 16 physical cores
//
// [Host] : .NET Framework 4.8.1 (4.8.9221.0), X86 LegacyJIT
// DefaultJob : .NET Framework 4.8.1 (4.8.9221.0), X86 LegacyJIT
//
//| Method | DictionarySize | Mean | Error | StdDev | Median | Ratio | RatioSD |
//|--------------------------- |--------------- |-----------:|----------:|-----------:|-----------:|------:|--------:|
//| TryGetValue_Miss_Guid | 500 | 4.816 μs | 0.0955 μs | 0.2251 μs | 4.784 μs | 1.00 | 0.07 |
//| TryGetValue_Miss_String_20 | 500 | 17.007 μs | 0.4316 μs | 1.2315 μs | 16.922 μs | 3.54 | 0.30 |
//| TryGetValue_Miss_String_50 | 500 | 29.642 μs | 0.5877 μs | 1.4744 μs | 29.379 μs | 6.17 | 0.42 |
//| | | | | | | | |
//| TryGetValue_Miss_Guid | 1000 | 9.519 μs | 0.2876 μs | 0.8299 μs | 9.203 μs | 1.01 | 0.12 |
//| TryGetValue_Miss_String_20 | 1000 | 30.069 μs | 0.5297 μs | 0.4955 μs | 30.063 μs | 3.18 | 0.26 |
//| TryGetValue_Miss_String_50 | 1000 | 60.970 μs | 1.2173 μs | 2.9630 μs | 60.627 μs | 6.45 | 0.60 |
//| | | | | | | | |
//| TryGetValue_Miss_Guid | 5000 | 57.775 μs | 1.3336 μs | 3.8902 μs | 57.355 μs | 1.00 | 0.09 |
//| TryGetValue_Miss_String_20 | 5000 | 166.241 μs | 2.7870 μs | 2.6069 μs | 166.367 μs | 2.89 | 0.20 |
//| TryGetValue_Miss_String_50 | 5000 | 327.829 μs | 6.4984 μs | 18.0069 μs | 319.527 μs | 5.70 | 0.49 |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment