FrozenDictionary is a specialized dictionary type in C# that offers significant performance improvements for read-only scenarios.
Imagine you have a dictionary that’s populated once and then only ever read from. Standard Dictionary<TKey, TValue> might seem fine, but under heavy read load, its internal structure can lead to inefficiencies. FrozenDictionary is engineered to eliminate these.
Here’s a simplified view of how it works. When you create a FrozenDictionary, it goes through an initial "freezing" process. This involves building a highly optimized, contiguous internal data structure. Think of it like packing a suitcase perfectly for a long trip – once it’s packed, you don’t rearrange things. After freezing, the dictionary is immutable. This immutability is key because it allows for aggressive optimizations that aren’t possible with a mutable dictionary.
Let’s see it in action. Suppose we have a mapping of country codes to their full names.
using System;
using System.Collections.Generic;
using System.Collections.Frozen; // Make sure to add this using directive
public class CountryLookup
{
private static readonly FrozenDictionary<string, string> _countryNames;
static CountryLookup()
{
var countryData = new Dictionary<string, string>
{
{ "US", "United States" },
{ "CA", "Canada" },
{ "MX", "Mexico" },
{ "GB", "United Kingdom" },
{ "DE", "Germany" },
{ "FR", "France" },
{ "JP", "Japan" },
{ "CN", "China" },
{ "IN", "India" },
{ "BR", "Brazil" }
};
_countryNames = countryData.ToFrozenDictionary(); // This is the key step
}
public static string GetCountryName(string code)
{
// Fast, read-only lookup
if (_countryNames.TryGetValue(code, out var name))
{
return name;
}
return "Unknown";
}
public static void Main(string[] args)
{
Console.WriteLine($"US: {GetCountryName("US")}");
Console.WriteLine($"DE: {GetCountryName("DE")}");
Console.WriteLine($"XX: {GetCountryName("XX")}");
}
}
The magic happens with countryData.ToFrozenDictionary(). This extension method, available when you include System.Collections.Frozen, takes an existing IEnumerable<KeyValuePair<TKey, TValue>> (like a Dictionary) and returns a FrozenDictionary<TKey, TValue>. The initial conversion has a cost, but subsequent reads are significantly faster.
The core problem FrozenDictionary solves is the overhead associated with mutable dictionaries. Even when you’re not modifying a Dictionary<TKey, TValue>, its internal hash table can involve pointer chasing and potential resizing (though resizing is less common in read-only scenarios). FrozenDictionary eliminates this by creating a static, contiguous array-based structure. Lookups become a direct index calculation and access, similar to accessing an array element. There’s no need for complex hashing logic per lookup or checking for concurrent modifications.
The primary lever you control is the initial conversion. The ToFrozenDictionary() method (and its ToFrozenSet() counterpart for sets) is the gateway. You can also construct a FrozenDictionary directly if you have the data in a suitable format, but using ToFrozenDictionary() on an existing collection is the most common pattern.
The real performance gains come from the fact that a FrozenDictionary can be shared across threads without any locking. Because it’s immutable, multiple threads can read from it concurrently without any risk of race conditions or the need for synchronization primitives like lock or ReaderWriterLockSlim. This makes it ideal for static data, configuration settings, or any lookup table that is initialized once and then heavily queried.
When you create a FrozenDictionary, especially from a large collection, the initial "freezing" process can be noticeable. This involves calculating all hash codes, arranging the elements into their final contiguous storage, and potentially performing optimizations like sorting or grouping based on hash values to minimize probe sequences. It’s a one-time cost for potentially millions of subsequent lookups. The FrozenDictionary doesn’t use a traditional linked list or separate buckets for collision resolution; instead, it uses techniques like linear probing or Robin Hood hashing within its contiguous array, which are highly cache-friendly.
The next concept you’ll want to explore is FrozenSet<T>, which applies the same principles of immutability and optimized storage to sets, offering similar read-only performance benefits.