Published on
- 4 min read
Speeding up Dictionary with structs

If you’re a C# developer, you’re probably familiar with the Dictionary
class. Most likely, you’ve used it with classes as values. But what if I told you that structs can be used instead — and you can even avoid performance issues from copying? Yes, there’s a way to do that, and it’s fast.
Disclaimer
The information here only applies in specific situations. Benchmark results may vary depending on hardware, compiler version, or runtime behavior. Always test your code yourself — don’t rely only on articles like this one.
Use Case
Let’s say you have a data array data
. Your task is to:
- Convert
data
into a dictionary. - Find a value by key and modify it.
- Repeat step 2 multiple times.
Here’s a simple version of that:
var dict = new Dictionary<int, SomeClass>(Length);
foreach (var (i, obj) in data) {
dict.Add(i, obj);
}
for (int i = 0; i < Length; i++) {
dict[i].DoWork(i);
}
The code works as expected. Let’s now replace SomeClass
with a SomeStruct
and see how the performance compares:
var dict = new Dictionary<int, SomeStruct>(Length);
foreach (var (i, obj) in data) {
dict.Add(i, obj);
}
for (int i = 0; i < Length; i++) {
var obj = dict[i];
obj.DoWork(i);
dict[i] = obj;
}
Benchmark
The test was run using an array of 100,000 elements. Struct and class sizes varied from 4 to 128 bytes. I used BenchmarkDotNet for performance testing. The code and results are available on GitHub.

Average execution time by struct size
The results show that performance drops when struct size exceeds 20 bytes. Structs are copied multiple times, and dictionary lookups happen twice. Let’s break this down to see how we can improve it.
Dictionary Initialization
The results are expected: initialization time grows linearly with struct size.

Avg. dictionary init time by struct size
This happens because the entries array stores struct values directly — not references. So, larger structs require more memory.
Technically, class-based versions also consume memory — but it happens earlier, during data
array creation. If we include array creation in the measurements, classes may perform worse. But that’s beyond this article.
Dictionary Fill
Again, results are as expected: fill time increases with struct size. Up to 20 bytes, the difference is small.

Avg. dictionary fill time by struct size
Lookup and Modify
For the third time — structs perform worse.

Avg. lookup + update time by struct size
This is because structs are copied twice during each access:
SomeStruct s = dict[i]; // 1st lookup + copy
s.DoWork(i);
dict[i] = s; // 2nd lookup + copy
This is where CollectionsMarshal
comes in handy.
What is CollectionsMarshal?
CollectionsMarshal
is a helper class with four methods:
AsSpan<T>
– gets a span from aList<T>
GetValueRefOrAddDefault<TKey, TValue>
– gets a reference by key or adds a default valueGetValueRefOrNullRef<TKey, TValue>
– gets a reference or returns a null-like refSetCount<T>
– sets theCount
of a list
We only care about the second and third methods. They allow us to get a reference to a value in the dictionary and avoid copying:
ref SomeStruct s = ref CollectionsMarshal.GetValueRefOrNullRef(dict, i);
s.DoWork(i);
More Benchmarks
Let’s benchmark this ref-based version and compare:

Avg. lookup + update time by struct size
It’s even faster than using classes! But this gain only matters if you have lots of lookup operations.

Benchmark breakdown by number of lookups
CollectionsMarshal Gotchas
Checking for default and null
As mentioned earlier, GetValueRefOrAddDefault
returns a reference to a default value. You can check if it existed using the out
flag:
ref var element = ref CollectionsMarshal.GetValueRefOrAddDefault(
dict, key, out bool exists);
if (exists) {
// safe to modify
}
With GetValueRefOrNullRef
, there’s no flag. Comparing to null
throws a NullReferenceException
. Instead, use Unsafe.IsNullRef
:
ref var element = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (Unsafe.IsNullRef(ref element)) {
// not found
}
Don’t modify the dictionary after getting a ref
The docs say clearly: don’t modify the dictionary after taking a reference. Here’s why:
ref var element = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
Console.WriteLine($"ref element: {element.Item1}"); // 30
Console.WriteLine($"dict[key]: {dict[key].Item1}"); // 30
element.Item1 = 50;
Console.WriteLine($"ref element: {element.Item1}"); // 50
Console.WriteLine($"dict[key]: {dict[key].Item1}"); // 50
dict.Add(100, new (100));
element.Item1 = 60;
Console.WriteLine($"ref element: {element.Item1}"); // 60
Console.WriteLine($"dict[key]: {dict[key].Item1}"); // 50 ← reference lost
Conclusion
Structs are an underrated feature in C#. Under the right conditions, they can boost performance. When using structs as dictionary values, consider using CollectionsMarshal
. The methods GetValueRefOrAddDefault
and GetValueRefOrNullRef
return references, allowing you to skip extra lookups and copies. This can significantly improve performance when there are many lookup operations.