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.
Top comments (0)