Benchmarking to the Bottom - Iterating Lists in .NET
Tags: dotnet, performance, programming, csharp
Published: 2022-03-10 12:06:54

Finding the fastest way to iterate lists 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 Lists

Today I decided to look at many different ways that you can iterate over a [list](https://medium.com/r/?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fapi%2Fsystem.collections.generic.list-1). The application I have been working on has many instances of long lists that are iterated for various reasons. It made me wonder what’s the fastest way to work through all the items in a [list](https://medium.com/r/?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fapi%2Fsystem.collections.generic.list-1).

On my initial run through, I neglected to assign the items being iterated to a variable. Purely iterating the [list](https://medium.com/r/?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fapi%2Fsystem.collections.generic.list-1) without accessing the item lead to skewed results. **Both sets of results** will be listed in the outcomes.

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)
- [LINQ ForEach](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.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

I have read lately about how the `for` loop is actually faster than the `foreach` one. I'm anticipating that what I would find is for that to be slightly more performant on a `List`. To cover multiple different ways to iterate through a list I have included LINQ and `for` as other options. I would anticipate LINQ to be slightly slower and I have never used `Span` before so I have no idea what to expect.

---

## 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 `List` 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/d1baf6a64abcac9d363bbe85554fe588 %}

---

## Methods

Next up is creating a method for each type of `List` 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 `List`, 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/a4202cea6fe91e731a7a3f1686cf50bf %}

---

## 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 | For           | 100   |    344.8 ns |
|    2 | ForEachLinq   | 100   |    559.9 ns |
|    3 | ForEach       | 100   |    659.4 ns |
|    3 | GetEnumerator | 100   |    659.5 ns |
|    4 | For           | 10000 | 33,562.8 ns |
|    5 | ForEachLinq   | 10000 | 53,895.5 ns |
|    6 | GetEnumerator | 10000 | 63,317.0 ns |
|    6 | ForEach       | 10000 | 63,356.6 ns |

### .NET 6.0

| Rank | Method        | N     |        Mean |
| ---: | ------------- | ----- | ----------: |
|    1 | SpanForEach   | 100   |    144.3 ns |
|    2 | SpanFor       | 100   |    176.3 ns |
|    3 | ForEach       | 100   |    251.0 ns |
|    3 | GetEnumerator | 100   |    251.0 ns |
|    3 | For           | 100   |    251.4 ns |
|    4 | ForEachLinq   | 100   |    352.1 ns |
|    5 | SpanForEach   | 10000 | 12,260.4 ns |
|    6 | SpanFor       | 10000 | 16,088.1 ns |
|    7 | ForEach       | 10000 | 24,109.4 ns |
|    7 | GetEnumerator | 10000 | 24,111.0 ns |
|    7 | For           | 10000 | 24,113.8 ns |
|    8 | ForEachLinq   | 10000 | 34,713.9 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

After running the benchmark tests, the huge surprise to me here is how bad `foreach` performs. It’s on par as the worst with `GetEnumerator` and gets outperformed by LINQ `foreach`. I will definitely not be using this in any older system any time soon. Would have been nice to have some `Span` results to compare but that feature is not available for .NET Framework. The clear choice here is to refactor all `List` iterations into `for` loops and live a happy life.

### .NET 6.0

The .NET results are quite surprising to me. I started running this not knowing much about `Span`, and now I’m ready to do a bit of research to understand more. When accessing the item, it appears that the `Span` has an edge over the `for`  and `foreach` and it looks like it's worth refactoring your entire codebase for that performance gain. The key outcome here is that any iteration of a `List` should look to convert that list to a `Span`.  Outside of that there is some difference between `for` and `foreach` when iterating a `Span` but I'm not sure that it's worth changing all those over.

### STOP USING LINQ FOREACH STATEMENTS

They are a performance killer in both **.NET 6.0** and **.NET Framework 4.8** and far slower than the alternatives.  Refactor those immediately. It's incredibly poor for something that does get used by a lot of people.

---

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