Benchmarking to the Bottom — Iterating Arrays in .NET
Tags: dotnet, performance, programming, csharp
Published: 2022-06-21 08:36:22

Finding the fastest way to iterate arrays while accessing their value in .NET using [BenchmarkDotNet](https://benchmarkdotnet.org) 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](https://medium.com/r/?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fcsharp%2Fprogramming-guide%2Farrays%2F). 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](https://benchmarkdotnet.org).

- [For](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/statements/iteration-statements#the-for-statement)
- [ForEach](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/statements/iteration-statements#the-foreach-statement)
- [Array ForEach](https://docs.microsoft.com/en-us/dotnet/api/system.array.foreach?view=net-6.0)
- [GetEnumerator](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1.getenumerator?view=net-6.0)
- [Span For](https://docs.microsoft.com/en-us/dotnet/api/system.span-1?view=net-6.0)
- [Span ForEach](https://docs.microsoft.com/en-us/dotnet/api/system.span-1?view=net-6.0)

---

##Theory

Given the similarity to the [previous test](https://dev.to/stphnwlsh/benchmarking-your-way-to-the-bottom-iterating-lists-1li5) 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](https://benchmarkdotnet.org) 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](https://benchmarkdotnet.org) is happy to give us a result. This should give us a reasonable assumption of the fastest way to do this going forwards.

{% embed https://gist.github.com/stphnwlsh/41540d724ffd8851f3fbf92529d0dead %}

---

## 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.

{% embed https://gist.github.com/stphnwlsh/64a0afde983ac8f1088059689bcd82f3 %}

---

## Results

Thankfully for us, [BenchmarkDotNet](https://benchmarkdotnet.org) 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](https://github.com/stphnwlsh/Benchmarking/actions).

---

## 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](https://github.com/stphnwlsh/Benchmarking). 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](https://www.linkedin.com/in/stphnwlsh), [Twitter](https://twitter.com/stphnwlsh) or [GitHub](https://github.com/stphnwlsh), and consider supporting me at [Buy Me a Coffee](https://www.buymeacoffee.com/stphnwlsh).

[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-1.svg)](https://www.buymeacoffee.com/stphnwlsh)