NDepend Blog

Improve your .NET code quality with NDepend

Alternate Lookup for Dictionary and HashSet in .NET 9

September 10, 2024 3 minutes read

Alternate Lookup for Dictionary and HashSet in .NET 9

In .NET 9, a new method called GetAlternateLookup<TKey, TValue, TAlternate>() has been introduced for hash tables classes, including Dictionary<TKey, TValue>, HashSet<T>, ConcurrentDictionary<TKey, TValue>, FrozenDictionary<TKey, TValue>, and FrozenSet<TKey, TValue>.

In which scenario can an alternate lookup improve performance?

The primary use case for an alternate key arises when TKey is a string. In such cases, strings are often represented as ReadOnlySpan<char>. Previously, to lookup a key represented by a ReadOnlySpan<char> in a hash table, it was necessary to convert it to a string using the ToString() method, which results in a string allocation. That is not optimal.

Alternate Lookup: Code Sample C# 13.0 .NET 9.0

With the introduction of the AlternateLookup<ReadOnlySpan<char>> struct, we can directly use ReadOnlySpan<char> as a key, avoiding unnecessary allocations.

There are two interesting points to highlight in this code sample:

  • Since .NET 8 the extension method Split<T> let’s obtain a SpanSplitEnumerator<T> which is also a ref struct.

Benchmarking Alternate Lookup

Here is the result of benchmarking alternate lookup with ReadOnlySpan<char> versus regular lookup with string. The alternate lookup version delivers a performance boost and zero allocations: all data operations are handled on the stack—how cool!

Here is the benchmark code:

How does the alternate lookup key work with StringComparer?

In the code sample above we are using StringComparer.OrdinalIgnoreCase. If we decompile the .NET 9 version of this class we can see that the comparer class implements the new interface IAlternateEqualityComparer<in TAlternate, T>:

This interface is defined this way:

Therefore, the OrdinalIgnoreCaseComparer implementation provides the Equals() and GetHashCode() methods that work with ReadOnlySpan<char>.

The AlternateLookup<T> struct allows modification on the underlying dictionary. It has methods such as public bool TryAdd(TAlternateKey key, TValue value). These modify-operations call the method IAlternateEqualityComparer.Create() to obtain a string object from a ReadOnlySpan<char>. This way they can insert a new entry in the dictionary from an alternate key.

If the comparer provided when creating the hash table doesn’t implement IAlternateEqualityComparer<in TAlternate, T> an exception is thrown at runtime when calling the method GetAlternateLookup().

Building our own alternate lookup

Here’s a small program that demonstrates how everything works together. We have a dictionary indexed by person objects, with each person having an ID used for indexing. This example shows how we can perform lookups using just the person’s ID, which is useful when the person object is unavailable.

It’s important to note that the Create() implementation cannot retrieve a person’s object from its ID, so the lookup should not be used to modify the dictionary. It’s worth considering the potential benefits of adding a GetAlternateReadOnlyLookup() method to effectively handle such scenarios where a key cannot be retrieved from its alternate version.

Conclusion

With .NET 9, there are even more scenarios where Span<T> can be leveraged to enhance performance by avoiding heap allocations and operating directly on the stack. This approach not only reduces memory overhead but also significantly improves execution speed, making it an essential tool for high-performance applications.

Leave a Reply

Your email address will not be published. Required fields are marked *