Comparing List Loop Performance in .NET: From .NET Framework to .NET 9-preview

Comparing List Loop Performance in .NET: From .NET Framework to .NET 9-preview

May 27, 2024ยท

5 min read

Loops are fundamental constructs in any programming language. But have you ever wondered if there are any performance differences between them? For small collections, the difference might be negligible, but what about handling a million or 10 million records? In such cases, will there be a noticeable performance difference?

In this article, we aim to determine the performance of different loops across various .NET target frameworks (.NET Framework 4.7.2, .NET Core 3.1, .NET 6.0, .NET 8.0, .NET 9.0-preview-4) when iterating over a List of strings.

Setup

To compare list loop performance, we will iterate over 10 million strings using a List<string>. To ensure consistency, we will generate the dataset using C#'s Random method with the same "seed" value.

We will utilize the BenchmarkDotNet library to benchmark the performance of various loops. This is a widely used library for benchmarking, even by the dotnet product teams.

Here is the snippet of our benchmark's "GlobalSetup" :

private readonly List<string> list = new List<string>();

// 10 million elements
private readonly int size = 10000000;

[GlobalSetup]
public void Setup()
{
    var random = new Random(580);
    for (int i = 0; i < size; i++)
    {
        list.Add(random.Next().ToString());
    }
}

The Loops

We will test the following looping methods on the list of strings:

  • for loop

  • foreach loop

  • List.ForEach loop

  • while loop

  • do-while loop

Additionally, we will compare performance across these dotnet versions:

  • .NET Framework 4.7.2

  • .NET Core 3.1

  • .NET 6.0

  • .NET 8.0

  • .NET 9.0-preview-4

Here is the benchmark test class:

[SimpleJob(RuntimeMoniker.Net472, baseline: true)]
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser]
public class ListLoopingPerformanceBenchmark
{
    private readonly List<string> list = new List<string>();

    // 10 million elements
    private readonly int size = 10_000_000;

    [GlobalSetup]
    public void Setup()
    {
        var random = new Random(580);
        for (int i = 0; i < size; i++)
        {
            list.Add(random.Next().ToString());
        }
    }

    [Benchmark(Description = "for")]
    public string For()
    {
        var result = string.Empty;
        var size = list.Count;
        for (int i = 0; i < size; i++)
        {
            result = list[i];
        }
        return result;
    }

    [Benchmark(Description = "foreach")]
    public string Foreach()
    {
        var result = string.Empty;
        foreach (var item in list)
        {
            result = item;
        }
        return result;
    }

    [Benchmark(Description = "ForEach")]
    public string ForEach()
    {
        var result = string.Empty;
        list.ForEach(item => result = item);
        return result;
    }

    [Benchmark(Description = "while")]
    public string While()
    {
        var result = string.Empty;
        int i = 0;
        int size = list.Count;
        while (i < size)
        {
            result = list[i];
            i++;
        }
        return result;
    }

    [Benchmark(Description = "do-while")]
    public string DoWhile()
    {
        var result = string.Empty;
        int i = 0;
        int size = list.Count;
        do
        {
            result = list[i];
            i++;
        } while (i < size);
        return result;
    }
}

For the complete code setup, you can visit GitHub repository.

Results

Based on the benchmarking setup, the results vary across different .NET versions, showcasing improvements over time. Here's a summary:

  • The for loop shows significant performance improvements from .NET Framework 4.7.2 to .NET 9.0-preview, with .NET 9.0-preview being 1.28x faster than the baseline.

  • The foreach loop exhibits substantial gains, especially from .NET Framework 4.7.2 to .NET 9.0-preview, being 3.82x faster.

  • The List.ForEach method is generally slower compared to other loops, though it shows some improvements in .NET 9.0-preview.

  • Both while and do-while loops demonstrate similar performance enhancements, with .NET 9.0-preview versions being approximately 1.30x faster than .NET Framework 4.7.2.

Bonus high performance looping with latest .NET

.NET 5 introduced CollectionsMarshal.AsSpan<T>, which provides direct access to the underlying array of a list as a span, providing a more efficient way to work with list elements. However, this should be used with caution, as modifying the list during iteration can lead to unexpected behavior.

Let's test the benchmark for this with different loop sizes of 1 million and 10 million records and see if it provides better results than looping over the list directly.

[SimpleJob(RuntimeMoniker.Net60, baseline: true)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser]
public class ListLoopingPerformanceBenchmark
{
    private readonly List<string> list = new List<string>();

    [Params(1_000_000, 10_000_000)]
    public int size;

    [GlobalSetup]
    public void Setup()
    {
        var random = new Random(580);
        for (int i = 0; i < size; i++)
        {
            list.Add(random.Next().ToString());
        }
    }

    // Adding `for` loop as comparision with `span`.
    [Benchmark(Description = "for")]
    public string For()
    {
        var result = string.Empty;
        var size = list.Count;
        for (int i = 0; i < size; i++)
        {
            result = list[i];
        }
        return result;
    }

    [Benchmark]
    public string Span()
    {
        var result = string.Empty;
        int size = list.Count;
        Span<string> span = CollectionsMarshal.AsSpan(list);

        for(var i = 0; i < size; i++)
        {
            result = span[i];
        }
        return result;
    }
}

Running these tests yields the following results for the Span loop:

Findings:

  • For smaller data sizes (1,000,000 elements):

    • The span loop in .NET 9.0-preview is almost 1.80x faster than in .NET 6.0 and nearly 1.6x faster when compared to for loop in .NET 9.0-preview.

    • This demonstrates a significant performance gain, making Span loops an attractive choice for handling smaller data sizes efficiently.

    • Do note the time taken to loop the entire list is mere micro seconds.

  • For larger data sizes (10,000,000 elements):

    • The Span loop in .NET 9.0-preview is 1.14x faster than in .NET 6.0 and nearly 1.14x faster when compared to for loop in .NET 9.0-preview.

    • Although the improvement is more moderate compared to smaller data sizes, it still shows noticeable performance gains.

  • This method should only be used when you understand its implications and there is a clear benefit.

Conclusion

This article explored the performance of different loop constructs in various .NET versions. We observed significant performance improvements over time, particularly from .NET Framework to .NET 9.

Except for the List.ForEach method, the other loop methods perform similarly in terms of time and memory usage in the latest .NET versions. The Span loop using CollectionsMarshal.AsSpan<T> method introduced in .NET 5 can offer substantial performance gains for small and medium data sizes.

However, while the Span loop offers excellent performance, it should be used with understanding of its implications and only when there is a clear benefit. Since the time difference between loops on a list of 10 millions records is very less, choosing the right loop construct should also consider readability, maintainability, and the specific use case requirements. By understanding these performance characteristics, developers can make more informed decisions to optimize their applications effectively.

For the complete code setup and benchmarks, visit GitHub repository.

Happy coding! ๐Ÿš€