NDepend Blog

Improve your .NET code quality with NDepend

.NET 9.0 LINQ Performance Improvements

November 19, 2024 5 minutes read

.NET 9.0 LINQ Performance Improvements NET 9.0 brings significant improvements to LINQ performance, with some scenarios showing remarkable gains. Let’s take a closer look at what’s driving these enhancements. The lessons learned will be relevant to your code.

Iterating with Span<T> when Possible

Let’s start by running this benchmark on .NET 8 versus .NET 9.

As a reminder, the .csproj file should look like this to run the benchmark with BenchmarkDotNet and the project must be compiled in Release mode.

Here are the results, which clearly speak for themselves.

The TryGetSpan() Method

In the post C# Array and List Fastest Loop, we demonstrated that using a Span<T> for iterating over an array is faster than regular for and foreach loops. In the benchmark above, the performance enhancement is primarily due to the use of the method TryGetSpan(). If the enumerable being iterated is an array or list, the method TryGetSpan() returns a ReadOnlySpan<T> for faster iteration. Here is the code extracted from TryGetSpan() to test if the source to enumerate is an array or a list, and then to obtain the span from the array or the list.

To me, this code does not look optimized.

  • source.GetType() is called twice!
  • Why not try to cast source to TSource[] or List<TSource> only once and then test the nullity of the obtained reference and use it?

This code was written by Stephen Toub and his team, who are THE .NET performance experts. They have a deep understanding of the C# compiler and JIT compiler optimizations, so it’s clear that this approach is the optimal one. The good news is that you can reuse this code in your own performance-critical code. And there is a lesson: In today’s highly optimized .NET stack, micro-optimizations in code are not obvious at all. Therefore, the advice to avoid premature optimization has never been more relevant.

One final note: List<TSource> internally references an array. When the list’s capacity needs to grow or shrink, a new array is created and then referenced. The call to CollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source)) retrieves a Span<TSource> from this internal array. Do you see the risk? If the list’s capacity changes somehow, the array obtained through this method might become invalid.

Definitely, the class System.Runtime.CompilerServices.Unsafe is well-named.

TryGetSpan() Callers

Now, let’s examine which methods call TryGetSpan(). Using NDepend, we scanned the assembly located at C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0-rc.1.24431.7\System.Linq.dll. From the TryGetSpan() method, we generated a code query to identify both direct and indirect callers. We then exported the 56 matched methods to the dependency graph. This analysis reveals that many standard Enumerable methods attempt to iterate over a span when the collection is an array or a list.

However, since holding the internal array of a list obtained via CollectionsMarshal.AsSpan() is not a safe option (as mentioned earlier), certain Enumerable operations that defer iteration (like when using the yield C# keyword) cannot rely on this optimization.

Call Graph To TryGetSpan

Specialized Iterators

Now let’s run the following benchmark found into this PR: Consolidate LINQ’s internal IIListProvider/IPartition into base Iterator class

The performance improvements are even more remarkable! What caused this?

The Astute

In summary, the .NET performance team designed the code to recognize common LINQ call chains. When such a chain is detected, some special iterators are created to handle the workflow more efficiently. Some more optimizations can happen when the chain ends up with methods like Count(), First(), Last(), ElementAt() or Sum(). For instance, OrderBy(criteria).First() can be optimized to execute as Min(criteria).

The implementation: Iterator<T> and its Derived Class

Let’s have a look at the abstract base class Iterator<T> and its 40 derivatives. They are all nested in the class Enumerable.

Iterator<T> is an abstract class but its methods are virtual. Hence its derivatives only override the required methods.

Iterators Methods

Here are the derivatives classes listed and exported to the graph:

Iterators Derived

Case Study: ListWhereSelectIterator<TSource, TResult>

Let’s focus on the iterator ListWhereSelectIterator<TSource, TResult>.

It is instantiated from the override of the Select() method in ListWhereIterator<TSource, TResult>.

ListWhereIterator<TSource, TResult> is instantiated within the Enumerable.Where() method using the following code:

The ListWhereSelectIterator<TSource, TResult> doesn’t override methods like TryGetFirst() or TryGetLast(), so how does it improve performance? The key optimization is that it acts as a single iterator for the supercommon Where(...).Select(...) chain on a list, which would typically require two separate iterators. By consolidating both operations into one, it inherently improves efficiency. You can see it in its implementation of MoveNext() where both delegates _predicate and _selector are invoked:

Case Study: IListSkipTakeIterator<TSource>

Here is the implementation of MoveNext() in the IListSkipTakeIterator<TSource> class:

Using the same approach as described above, this iterator is instantiated when applicable. Its optimization lies in avoiding unnecessary iteration by skipping elements that fall outside the _minIndexInclusive and _maxIndexInclusive range.

Conclusion

With .NET 9, LINQ becomes faster in several common scenarios. As with every new version of .NET, you simply need to migrate and recompile to take advantage of these improvements. Additionally, LINQ has been optimized in other ways: SIMD is utilized whenever possible, such as when summing a sequence of integers. Moreover, enumerating empty sequences incurs lower costs due to early detection.

If you have the opportunity, I highly recommend watching the DeepDotnet videos featuring Scott Hanselman and Stephen Toub. If your schedule is tight, consider using work hours for this, and explain to your boss that it’s valuable learning time.

 

One final note: the web is increasingly inundated with AI-generated crap content. Search engines struggles to differentiate between valuable, handcrafted content and inferior material. If you appreciate this article and others like it that are thoughtfully created, please consider sharing it.

 

 

Leave a Reply

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