DEV Community

Stephen Walsh
Stephen Walsh

Posted on

Benchmarking to the Bottom — Iterating Arrays in .NET

Finding the fastest way to iterate arrays while accessing their value in .NET using BenchmarkDotNet validate performance at scale.


I have spent some time recently looking through an older system, wondering how I can make it perform better. There are many avenues to make a system like this faster and more performant, but I wanted to avoid large scale refactoring to keep the risk to a minimum. Specifically I have focused on small items that can add up to make a bigger difference overall. The most obvious is a whole system upgrade but when that’s not an option, these small changes might be all an engineer has to make a difference.

Doing this work has peaked my interest as to which is the best way of doing these every day small tasks in the first place. I’m slowly building out a small series of tasks. It might be consuming different libraries to complete a task vs the native features, or it might straight up comparisons of every day tasks. Either way lessons will be learned and fun will be had.


Iterating and Accessing Arrays

Today I decided to look at many different ways that you can iterate over an Array. It's not complicated in any way but given that software engineers are constantly using both lists and arrays in C#, there is value in addressing them as well.
 
Key to the outcome is actually assigning the item from the array to a variable. This will ensure that the value is being read, purely iterating without accessing the item could lead to skewed results.

Below are the methods being used and tested via BenchmarkDotNet.


Theory

Given the similarity to the previous test I would imagine that the Span methods are going to be our clear winner here. With no LINQ methods to pick on I'll choose the GetEnumerator method to be the worst performing out of them all. I anticipate that the for loop will be fairly close to the Span and foreach will be a little further back.


Setup

Using BenchmarkDotNet to create a console application makes running this benchmarking test very simple. To ensure older systems are being covered these tests will be run in both .NET Framework 4.8 and .NET 6.0 as these are the current Long Term Support versions of .NET. One for the older world and one for the new world.

The premise is simple. Create a Array of strings 100 and 10,000 deep. Then run the test to iterate through each item and do this on repeat until BenchmarkDotNet is happy to give us a result. This should give us a reasonable assumption of the fastest way to do this going forwards.


Methods

Next up is creating a method for each type of Array iteration that I want to test. Each method does one thing to make it an even benchmark. Each method will iterate through each item in the Array, access the item in the list and assign it to a variable. Once the all the items have been process it will finish and record the time taken to complete the task.


Results

Thankfully for us, BenchmarkDotNet includes a Rank column to make it clear which run of tests is the winner. As you read these results you can use the Mean column to compare the difference in runtime between each test. For some of these there is a clear winner and others the difference is negligible.

.NET Framework 4.8

Rank Method N Mean
1 ForEachLoop 100 305.9 ns
2 ForLoop 100 343.9 ns
3 ArrayForEach 100 451.1 ns
4 GetEnumerator 100 10,561.3 ns
5 ForEachLoop 10000 30,064.2 ns
6 ForLoop 10000 33,541.9 ns
7 ArrayForEach 10000 43,825.3 ns
8 GetEnumerator 10000 1,055,741.5 ns

.NET 6.0

Rank Method N Mean
1 SpanForEach 100 141.9 ns
2 ForLoop 100 173.6 ns
2 ForEachLoop 100 175.2 ns
2 SpanFor 100 175.5 ns
3 ArrayForEach 100 370.7 ns
4 GetEnumerator 100 6,190.5 ns
5 SpanForEach 10000 12,333.7 ns
6 ForLoop 10000 16,181.6 ns
6 SpanFor 10000 16,187.0 ns
6 ForEachLoop 10000 16,251.8 ns
7 ArrayForEach 10000 36,155.1 ns
8 GetEnumerator 10000 611,853.5 ns

Results are formatted for clean and clear display, for the full output you peruse the output of the actions on GitHub.


Thoughts

.NET Framework 4.8

Well quite honestly this is quite the surprise, from the test results foreach is the clear winner. Given it's terrible performance on a List, I really didn't see this coming. It's faster at iterating the larger and smaller array than the for loop. Arrays have a new champion. As expected GetEnumerator is the worst performer by absolute leaps and bounds. The outcome here is clear though, time to refactor your code if you're Array.Foreach in favour of foreach. You could take it or leave it with the for loops as the difference is not massive.

.NET 6.0

It's a familiar story for .NET 6.0 the Span foreach methods taking home the prize. It's closely followed by for, foreach and Span for which would lead me to say that it's not worth a major refactoring effort here. There's only minor gains to be made, but any new code should use a Span combined with a foreach. Array.ForEach is well behind here to and totally worth the refactor. GetEnumerator has been put down enough, it's just not worth your time.

ForEach FTW

While not life changing in some areas the foreach loop is the clear winner and should be implemented as the iterator of choice for .NET 6.0 and .NET Framework 4.8 applications. Just remember with .NET 6.0 to make it a Span as well.


Code

All of this code is open sourced. You can find my benchmarking work on GitHub. The results are the output of a GitHub Action to run the benchmark tests, you should be able to find detailed output on that repository.


Support

If you like this, or want to checkout my other work, please connect with me on LinkedIn, Twitter or GitHub, and consider supporting me at Buy Me a Coffee.

"Buy Me A Coffee"

Top comments (0)