Frozen Collections in .NET 8

sharmila subbiah
.Net Programming
Published in
3 min readMay 22, 2024

Frozen collections are specialized collections that offer both the benefits of immutability and enhanced performance. Once created, these collections cannot be modified, ensuring thread safety and reducing the need for synchronization in concurrent applications. They are designed to provide faster access times compared to traditional immutable collections due to optimizations made during their creation phase.

Key Features

Immutability: Frozen collections, like other immutable collections, cannot be changed once created. This guarantees data consistency and thread safety without requiring locks or other synchronization mechanisms.

Performance Optimization: These collections are specifically optimized for read operations. They reduce memory overhead and enhance lookup times during initialization, offering superior performance in scenarios where read operations significantly outnumber write operations.

Thread Safety: Being immutable, frozen collections are inherently thread-safe. Multiple threads can read from the same frozen collection without the risk of data races or complex synchronization.

Ease of Use: Frozen collections are easy to integrate into existing codebases. They offer familiar APIs and can be used wherever traditional collections are used, with the added benefits of immutability and performance enhancements.

Types of Frozen Collections

.NET 8 introduces several types of frozen collections, each optimized for different use cases:

FrozenDictionary<TKey,TValue>: This is a read-only dictionary optimized for fast key lookups. It is ideal for scenarios where the dictionary is created once and read frequently.

using System.Collections.Frozen;

var dictionary = new Dictionary<int, string>
{
{ 1, "One" },
{ 2, "Two" },
{ 3, "Three" }
};

// Convert to a FrozenDictionary
var frozenDictionary = dictionary.ToFrozenDictionary();

// Access elements
Console.WriteLine(frozenDictionary[1]); // Output: One
Console.WriteLine(frozenDictionary[2]); // Output: Two

FrozenSet<T>: A read-only set that provides fast lookup times for checking the presence of elements.

using System.Collections.Frozen;

var set = new HashSet<string> { "Apple", "Banana", "Cherry" };

// Convert to a FrozenSet
var frozenSet = set.ToFrozenSet();

// Check for the presence of elements
Console.WriteLine(frozenSet.Contains("Apple")); // Output: True
Console.WriteLine(frozenSet.Contains("Grape")); // Output: False

Benchmarking Collection Creation

To compare the creation performance of different collections, including List, HashSet, ImmutableHashSet, and FrozenSet, we can use the BenchmarkDotNet library.

Benchmark Setup

using BenchmarkDotNet.Attributes;
using System.Collections.Frozen;
using System.Collections.Immutable;

namespace FrozenCollections;

public class CollectionCreationBenchmark
{
private readonly List<int> _data;

public CollectionCreationBenchmark()
{
_data = Enumerable.Range(0, 10000).ToList();
}

[Benchmark]
public List<int> CreateList()
{
return new List<int>(_data);
}

[Benchmark]
public HashSet<int> CreateHashSet()
{
return new HashSet<int>(_data);
}

[Benchmark]
public ImmutableHashSet<int> CreateImmutableHashSet()
{
return ImmutableHashSet.CreateRange(_data);
}

[Benchmark]
public FrozenSet<int> CreateFrozenSet()
{
return _data.ToFrozenSet();
}
}

Output:

Interpretation: CreateList is typically the fastest due to its simple structure.CreateHashSet takes longer due to the overhead of ensuring uniqueness.CreateImmutableHashSet is slower because it involves creating an immutable structure.CreateFrozenSet falls between HashSet and ImmutableHashSet, reflecting its optimized read access after creation.

Benchmarking the Lookup Performance

Next, we benchmark the lookup performance for these collections.

Benchmark Setup

using BenchmarkDotNet.Attributes;
using System.Collections.Frozen;
using System.Collections.Immutable;

namespace FrozenCollections;

public class CollectionLookupBenchmark
{
private readonly List<int> _data;
private readonly List<int> _list;
private readonly HashSet<int> _hashSet;
private readonly ImmutableHashSet<int> _immutableHashSet;
private readonly FrozenSet<int> _frozenSet;

public CollectionLookupBenchmark()
{
_data = Enumerable.Range(0, 10000).ToList();
_list = new List<int>(_data);
_hashSet = new HashSet<int>(_data);
_immutableHashSet = ImmutableHashSet.CreateRange(_data);
_frozenSet = _data.ToFrozenSet();
}

[Benchmark]
public bool ListLookup()
{
return _list.Contains(5000);
}

[Benchmark]
public bool HashSetLookup()
{
return _hashSet.Contains(5000);
}

[Benchmark]
public bool ImmutableHashSetLookup()
{
return _immutableHashSet.Contains(5000);
}

[Benchmark]
public bool FrozenSetLookup()
{
return _frozenSet.Contains(5000);
}
}

Output

Interpretation:

ListLookup is generally slower because it requires a linear search.HashSetLookup is much faster due to the constant time complexity of hash-based lookups.FrozenSetLookup outperforms, reflecting its optimized structures for read operations.ImmutableHashSetLookup is the slowest

Conclusion

Frozen collections in .NET 8 provide developers with a powerful tool for creating high-performance, immutable data structures. Their thread safety and optimized read performance make them ideal for many applications, particularly those involving configuration data, lookup tables, and cached data.

--

--

sharmila subbiah
.Net Programming

With over a decade of experience in the tech industry, I currently hold the position of Senior Software Engineer at Youlend.