NDepend Blog

Improve your .NET code quality with NDepend

C# Array and List Fastest Loop in 2024

June 12, 2024 5 minutes read
C# Array and List Fastest Loop
Discussions about the fastest way to loop through an array (T[]) and a List<T> in C# have been ongoing. Results can vary with each .NET version update. To get a clearer picture, we conducted our own benchmarks using .NET 8 and BenchmarkDotNet.

At NDepend we value your time, so here are our key findings:

  • When iterating over a C# array, the foreach way is the quickest.
  • In a for loop, storing the array.Length or list.Count in a variable does not improve performance and may actually decrease it.
  • for loop is most efficient on a List<T> than foreach.
  • However, the most efficient way to loop over a List<T> involves accessing the internal array via a Span<T> using Span<int> span = CollectionsMarshal.AsSpan(list). This approach is beneficial in performance-critical situations, provided that no elements are added or removed from the list.
  • Using a lambda for iterating the LINQ way, like in Array.Foreach(array, item => ...) or list.ForEach(item => ...) is unsurprisingly, way slower than regular for and foreach loops.

Let’s go through the experiments and investigations that led us to these conclusions:

Array int[] Fastest Loop

Here are the results obtained through 9 ways to loop over an int[].

C# Loop Array Result

Here is the code that you can copy straight into a Program.cs file, assuming the project imports BenchmarkDotNet:


foreach wins, why?

While the foreach loop over an array can sometimes be marginally slower than when using Span<T>, the difference is negligible. Additionally, introducing Span<T> can complicate the code, which may not justify the tiny performance benefit.

However, let’s investigate why foreach is faster than for. Let’s use SharpLab to decompile this program and investigate:

Here is the result from the C# compiler:

These are quite similar. However the foreach loop introduces a reference to the array as a local variable. This modification enables the JIT compiler to eliminate bounds checking, significantly speeding up each loop. Observe the variations in the generated assembly code. To make it easier to read, the loop is highlighted in both cases.

C# Loop Array Assembly

Managed pointers and the C# keyword ref

The ForSpanRefLoop() and ForSpanRefLoop2() ways are extracted from two Nick Chapsas’ videos here and here. They involve ref variables. In C# the keyword ref means managed pointers. In C# 1.0 the keyword ref was only used for passing parameters by reference. Since C# 7.0 almost every version of C# adds new areas where managed pointers can be used: ref variable, ref return, struct ref

Managed pointers constitute a unique feature of the .NET runtime that offers considerable benefits:

  • They can target various types of memory: local variables, in or out method parameters, stack locations, heap objects, object fields, array elements, strings, string locations, and unmanaged memory buffers.
  • The Garbage Collector recognizes managed pointers and adjusts them appropriately when the targeted objects are relocated.
  • They function efficiently.

The main restriction is that managed pointers must remain on the thread stack. This restriction also makes them faster than usual .NET references since the location of a managed pointer cannot change during a GC operation.

Span<T> implementation is based on managed pointers. All in all, Span<T> is a way to work with pointers like in C, in a safe context even though calls to method like Unsafe.Add() makes the code, well, unsafe.

While Span<T> doesn’t help improve performance when looping over an array, we’ll see that it is valuable when looping over a List<T>.

References:

List<T> Fastest Loop

We slightly modified the program above to make it work on a List<int>. Here are the results:


Array vs. List Loop

First, let’s compare array and list results. Looping over a list is generally slower than looping over an array. This makes sense since List<T> internally holds a T[]. Thus, it necessarily adds a layer of complexity.

Span<T> makes list loops faster

The real surprise is that using a Span<T> to loop over a list is significantly faster than for and foreach regular ways. What happens is that the call to CollectionsMarshal.AsSpan(list) obtains a managed pointer to the list’s internal array. This call is generally a bad idea because if some elements are added or removed, the list might internally allocate a new array. Thus the span obtained now points toward void memory. This is demonstrated by this program:

However, assuming that the number of elements in the list and its capacity doesn’t get modified, using CollectionsMarshal.AsSpan(list) does help improve the performance of looping over a list in critical scenarios.

for loop is faster on list than foreach

Unlike array, for loops are faster on lists than foreach loop. The reason is obvious when investigating this program in SharpLap:

The C# compiler produces this code. The call to enumerator.MoveNext() and the try/catch block necessarily degrade the performances:

Conclusion

We listed the key findings in the introduction. Let’s conclude that future versions of .NET and C# might change these results. For example, in the future, the C# compiler might assert that a list doesn’t get modified and use the Span<T> based optimization. We will update this post regularly to find out.

As passionate programmers, we find all these insights intriguing. However, it’s important to remember that in most scenarios, code readability outweighs the benefits of minor performance enhancements. Therefore, let’s reserve the use of MemoryMarshal, CollectionsMarshal, and Unsafe stuff for those parts of the code where performance is absolutely critical.

Comments:

  1. GrumpyOldDev says:

    Forward this article to the Sonar folks. It’s constantly yelling to use linq versus a foreach, despite ther performance hit. This making code reviews… slower.

Comments are closed.