Optimizing Array Iteration in .NET with BenchmarkDotNet
Written on
Recently, I delved into an older system, seeking ways to enhance its performance. There are numerous approaches to accelerate a system like this, but my aim was to avoid extensive refactoring to minimize risk. Instead, I concentrated on smaller adjustments that could cumulatively yield significant results. While a comprehensive system upgrade is the ideal solution, sometimes these incremental changes are all an engineer has to effect improvement.
This endeavor sparked my curiosity about the most efficient methods for executing everyday tasks. I’m gradually compiling a small series of tests that may involve comparing various libraries with built-in features or directly contrasting common operations. Regardless of the specifics, valuable insights will emerge along the way.
Iterating and Accessing Arrays
Today, I explored various techniques for iterating through an Array. While the process isn’t overly complex, it’s essential given that software engineers frequently work with both lists and arrays in C#.
The critical factor in achieving accurate results is assigning the array item to a variable. Simply iterating without accessing the item could yield misleading outcomes.
Below are the techniques that were implemented and assessed using BenchmarkDotNet:
- For
- ForEach
- Array ForEach
- GetEnumerator
- Span For
- Span ForEach
Theory
Based on the similarities to previous tests, I expect the Span methods to emerge as the top performers. Without LINQ methods to critique, I anticipate that the GetEnumerator method will lag behind the others. I expect the for loop to be competitive with the Span and that foreach will trail slightly.
Setup
Using BenchmarkDotNet to create a console application simplifies running these benchmarking tests. To ensure comprehensive coverage, tests will be executed in both .NET Framework 4.8 and .NET 6.0, representing the current Long-Term Support versions. This will allow us to compare the older and newer environments.
The test's premise is straightforward: create an Array containing 100 and 10,000 elements, then iterate through each element, repeating the process until BenchmarkDotNet provides satisfactory results. This will help determine the fastest method moving forward.
Methods
Next, I’ll develop a method for each type of Array iteration I wish to evaluate. Each method will perform a single task to ensure a fair comparison. Every method will iterate through all items in the Array, access each item, and assign it to a variable. Upon completion of all items, the method will record the time taken for the operation.
Results
Fortunately, BenchmarkDotNet offers a Rank column that clearly indicates the top-performing tests. You can refer to the Mean column to see the runtime differences among each test. Some methods yield clear winners, while others show minimal variance.
.NET Framework 4.8 — Iterating and Accessing
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 — Iterating and Accessing
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 |
Thoughts
.NET Framework 4.8
Surprisingly, the test results indicate that foreach is the clear champion. Given its poor performance with a List, this outcome was unexpected. It proved faster than the for loop for both array sizes. Arrays have found a new champion. As anticipated, GetEnumerator performed the worst by a significant margin. The takeaway is clear: if you are using Array.ForEach, it’s time to refactor your code to utilize foreach. The difference with for loops is negligible and can be taken or left.
.NET 6.0
In .NET 6.0, the story remains familiar with the Span foreach methods taking the lead. They are closely followed by for, foreach, and Span for, suggesting that major refactoring isn’t necessary. While only minor improvements can be made, any new development should prioritize using Span alongside foreach. Array.ForEach is significantly lagging and merits a refactor. GetEnumerator has been criticized enough; it simply isn't worth the effort.
ForEach FTW
Although the differences may not be groundbreaking in all contexts, the foreach loop stands out as the clear choice for iterators in both .NET 6.0 and .NET Framework 4.8 applications. Just remember to use Span with it in .NET 6.0.
Code
All of this code is open source. You can explore my benchmarking work on GitHub. The results are derived from a GitHub Action that runs the benchmark tests, and you should find detailed output in that repository.
Support
If you appreciate this content or wish to explore more of my work, feel free to connect with me on LinkedIn, Twitter, or GitHub.