.NET 8 — Frozen Collections
Frozen Collections is a new .NET 8 feature that can be used to create Dictionaries and Sets for faster read operations when you don’t need to make changes after the creation. In this article, I present how to work with these collections and demonstrate the performance difference when compared with other collections.
The new System.Collections.Frozen
namespace includes the collection types FrozenDictionary<TKey,TValue>
and FrozenSet<T>
, these types are optimized for fast lookup operations. They take a bit more time during the creation, but the read operations are faster when compared with a Dictionary
or a Set
.
FrozenDictionary and FrozenSet
A FrozenDictionary
and FrozenSet
provides an immutable, read-only dictionary
and set
, optimized for fast lookup and enumeration. It is optimized for cases when you only need to create a dictionary/set once, and will not need to change keys or values, and it is frequently used at the run time.
They are ideal for cases when the dictionary/set is created once (potentially at the startup of the application) and used throughout the remainder of the life of the app.
To create a FrozenDictionary
you can use the ToFrozenDictionary
method:
FrozenDictionary<int, int> frozenDictionary =
Enumerable.Range(0, 10).ToFrozenDictionary(key => key);
To create a FrozenSet
you can use the ToFrozenSet
method:
FrozenSet<int> frozenSet = Enumerable.Range(0, 10).ToFrozenSet();
Benchmark
For demonstration purposes, I create a series of methods that do three different operations for different collection types: Create the collection, execute the TryGetValue method, and execute a Lookup operation, and to run the benchmark, I used the BenchmarkDotNet
package.
To execute the benchmarks you need to set the Visual Studio to Release
and execute the project, or use the following commands via the command line (When running via the command line, the results will be stored in a folder named BenchmarkDotNet.Artifacts
):
// Build:
dotnet build -c Release .\FrozenCollections
// Run:
dotnet FrozenCollections\bin\Release\net8.0\FrozenCollections.dll
In the benchmark result you can see the following information:
- Method: the name of the benchmarked method.
- Mean: Arithmetic mean of all measurements.
- Error: Half of 99.9% confidence interval.
- StdDev: Standard deviation of all measurements. A lower standard deviation indicates more consistent results.
- Rank: the rank performance position (from fastest to slowest).
For the benchmark results, smaller values are better.
Create benchmark
In the class below you can find the methods that create a series of collection types:
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[MemoryDiagnoser(false)]
[RankColumn(NumeralSystem.Arabic)]
[MarkdownExporter]
public class CreateBenchmark
{
private const int itemsCount = 10_000;
[Benchmark]
public void CreateDictionary()
{
Dictionary<int, int> dictionary = Enumerable.Range(0, itemsCount).ToDictionary(key => key);
}
[Benchmark]
public void CreateImmutableDictionary()
{
ImmutableDictionary<int, int> dictionary = Enumerable.Range(0, itemsCount).ToImmutableDictionary(key => key);
}
[Benchmark]
public void CreateFrozenDictionary()
{
FrozenDictionary<int, int> frozenDictionary = Enumerable.Range(0, itemsCount).ToFrozenDictionary(key => key);
}
[Benchmark]
public void CreateList()
{
List<int> list = Enumerable.Range(0, itemsCount).ToList();
}
[Benchmark]
public void CreateImmutableList()
{
ImmutableList<int> list = Enumerable.Range(0, itemsCount).ToImmutableList();
}
[Benchmark]
public void CreateHashSet()
{
HashSet<int> hashSet = Enumerable.Range(0, itemsCount).ToHashSet();
}
[Benchmark]
public void CreateImmutableHashSet()
{
ImmutableHashSet<int> hashSet = Enumerable.Range(0, itemsCount).ToImmutableHashSet();
}
[Benchmark]
public void CreateFrozenSet()
{
FrozenSet<int> frozenSet = Enumerable.Range(0, itemsCount).ToFrozenSet();
}
}
This is the benchmark result:
| Method | Mean | Error | StdDev | Rank |
|-------------------------- |------------:|----------:|----------:|-----:|
| CreateList | 141.0 μs | 2.41 μs | 2.78 μs | 1 |
| CreateDictionary | 842.7 μs | 12.07 μs | 9.43 μs | 2 |
| CreateHashSet | 1,038.0 μs | 20.64 μs | 40.25 μs | 3 |
| CreateFrozenSet | 1,811.1 μs | 20.12 μs | 16.80 μs | 4 |
| CreateImmutableList | 2,706.9 μs | 50.81 μs | 92.90 μs | 5 |
| CreateFrozenDictionary | 2,708.1 μs | 53.33 μs | 121.47 μs | 5 |
| CreateImmutableHashSet | 19,103.4 μs | 370.51 μs | 576.84 μs | 6 |
| CreateImmutableDictionary | 21,426.4 μs | 422.12 μs | 935.38 μs | 7 |
The creation of a FrozenDictionary
and a FrozenSet
are slower when compared with the creation of other collections, but as we are going to see in the next methods, the reading operation will be faster.
TryGetValue benchmark
In the class below, you can find a series of collection types being initialized as private properties, with 100.000 items. Each class method executes the TryGetValue
method, searching for the key 500:
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn(NumeralSystem.Arabic)]
[MarkdownExporter]
public class TryGetValueBenchmark
{
private const int itemsCount = 100_000;
private const int keyToFind = 500;
private Dictionary<int, int> _dictionary = Enumerable.Range(0, itemsCount).ToDictionary(key => key);
private ImmutableDictionary<int, int> _immutableDictionary = Enumerable.Range(0, itemsCount).ToImmutableDictionary(key => key);
private HashSet<int> _hashSet = Enumerable.Range(0, itemsCount).ToHashSet();
private ImmutableHashSet<int> _immutableHashSet = Enumerable.Range(0, itemsCount).ToImmutableHashSet();
private FrozenDictionary<int, int> _frozenDictionary = Enumerable.Range(0, itemsCount).ToFrozenDictionary(key => key);
private FrozenSet<int> _frozenSet = Enumerable.Range(0, itemsCount).ToFrozenSet();
[Benchmark]
public void TryGetValueDictionary()
{
_dictionary.TryGetValue(keyToFind, out _);
}
[Benchmark]
public void TryGetValueImmutableDictionary()
{
_immutableDictionary.TryGetValue(keyToFind, out _);
}
[Benchmark]
public void TryGetValueFrozenDictionary()
{
_frozenDictionary.TryGetValue(keyToFind, out _);
}
[Benchmark]
public void TryGetValueHashSet()
{
_hashSet.TryGetValue(keyToFind, out _);
}
[Benchmark]
public void TryGetValueImmutableHashSet()
{
_immutableHashSet.TryGetValue(keyToFind, out _);
}
[Benchmark]
public void TryGetValueFrozenSet()
{
_frozenSet.TryGetValue(keyToFind, out _);
}
}
This is the benchmark result:
| Method | Mean | Error | StdDev | Rank |
|------------------------------- |----------:|----------:|----------:|-----:|
| TryGetValueFrozenDictionary | 1.622 ns | 0.0592 ns | 0.0904 ns | 1 |
| TryGetValueFrozenSet | 2.905 ns | 0.0833 ns | 0.2251 ns | 2 |
| TryGetValueDictionary | 3.429 ns | 0.0925 ns | 0.0909 ns | 3 |
| TryGetValueHashSet | 3.723 ns | 0.1010 ns | 0.2259 ns | 4 |
| TryGetValueImmutableHashSet | 17.261 ns | 0.3619 ns | 0.6145 ns | 5 |
| TryGetValueImmutableDictionary | 17.841 ns | 0.3823 ns | 0.7894 ns | 6 |
Executing the TryGetValue
in a FrozenDictionary
and in a FrozenSet
were the fastest operations when compared with these other collections.
Lookup Benchmark:
In the class below, you can find a series of collection types being initialized as private properties, with 100.000 items. On each class method, there is a loop (using a for
), and for each iteration the method ContainsKey
is executed:
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn(NumeralSystem.Arabic)]
[MarkdownExporter]
public class LookupBenchmark
{
private const int itemsCount = 100_000;
private const int iterations = 1_000;
private Dictionary<int, int> _dictionary = Enumerable.Range(0, itemsCount).ToDictionary(key => key);
private ImmutableDictionary<int, int> _immutableDictionary = Enumerable.Range(0, itemsCount).ToImmutableDictionary(key => key);
private List<int> _list = Enumerable.Range(0, itemsCount).ToList();
private ImmutableList<int> _immutableList = Enumerable.Range(0, itemsCount).ToImmutableList();
private HashSet<int> _hashSet = Enumerable.Range(0, itemsCount).ToHashSet();
private ImmutableHashSet<int> _immutableHashSet = Enumerable.Range(0, itemsCount).ToImmutableHashSet();
private FrozenDictionary<int, int> _frozenDictionary = Enumerable.Range(0, itemsCount).ToFrozenDictionary(key => key);
private FrozenSet<int> _frozenSet = Enumerable.Range(0, itemsCount).ToFrozenSet();
[Benchmark]
public void LookupDictionary()
{
for (int i = 0; i < iterations; i++)
_ = _dictionary.ContainsKey(i);
}
[Benchmark]
public void LookupImmutableDictionary()
{
for (int i = 0; i < iterations; i++)
_ = _immutableDictionary.ContainsKey(i);
}
[Benchmark]
public void LookupFrozenDictionary()
{
for (var i = 0; i < iterations; i++)
_ = _frozenDictionary.ContainsKey(i);
}
[Benchmark]
public void LookupList()
{
for (int i = 0; i < iterations; i++)
_ = _list.Contains(i);
}
[Benchmark]
public void LookupImmutableList()
{
for (int i = 0; i < iterations; i++)
_ = _immutableList.Contains(i);
}
[Benchmark]
public void LookupHashSet()
{
for (var i = 0; i < iterations; i++)
_ = _hashSet.Contains(i);
}
[Benchmark]
public void LookupImmutableHashSet()
{
for (var i = 0; i < iterations; i++)
_ = _immutableHashSet.Contains(i);
}
[Benchmark]
public void LookupFrozenSet()
{
for (var i = 0; i < iterations; i++)
_ = _frozenSet.Contains(i);
}
}
This is the benchmark result:
| Method | Mean | Error | StdDev | Rank |
|-------------------------- |-------------:|-----------:|-----------:|-----:|
| LookupFrozenSet | 2.016 μs | 0.0401 μs | 0.0763 μs | 1 |
| LookupFrozenDictionary | 2.280 μs | 0.0438 μs | 0.0875 μs | 2 |
| LookupHashSet | 3.437 μs | 0.0681 μs | 0.1060 μs | 3 |
| LookupDictionary | 3.647 μs | 0.0609 μs | 0.0540 μs | 4 |
| LookupList | 24.507 μs | 0.4882 μs | 0.7455 μs | 5 |
| LookupImmutableHashSet | 30.181 μs | 0.5236 μs | 0.4898 μs | 6 |
| LookupImmutableDictionary | 35.969 μs | 0.7154 μs | 1.4122 μs | 7 |
| LookupImmutableList | 1,413.501 μs | 27.7182 μs | 47.8125 μs | 8 |
As expected, the lookup in a FrozendDictionary
and in a FrozenSet
were the fastest when compared with the other collections.
Conclusion
Frozen Collections are collections optimized for situations where you have collections that will be frequently accessed, and you do not need to change the keys and values after creating. These collections are a bit slower during the creation, but reading operations are faster.
This is the link for the project in GitHub: https://github.com/henriquesd/FrozenCollections
If you like this demo, I kindly ask you to give a ⭐️ in the repository.
Thanks for reading!