Frozen Collections in .NET 8
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.