Why is ForEach Iteration over List<T> faster than IList<T>

In this post, I would like to draw your attention to the performance implications when iterating over IList<T> and List<T> .

First let us begin by writing some code to run our benchmark tests against. We will execute a simple foreach loop against IList<T> and List<T>.

[Benchmark]
[ArgumentsSource(nameof(IListCollection))]
public void ForEachIList(IList<int> collection)
{
    foreach (var item in collection)
    {
        DoSomething(item);
    }
}

[Benchmark]
[ArgumentsSource(nameof(ListCollection))]
public void ForEachList(List<int> collection)
{
    foreach (var item in collection)
    {
        DoSomething(item);
    }
}

It would be interesting to observe the benchmark results for the above when collection size is about 100000 items.

|             Method |      Mean |     Error |    StdDev |
|------------------- |----------:|----------:|----------:|
|       ForEachIList | 10.743 ms | 0.1160 ms | 0.1085 ms |
|        ForEachList |  6.229 ms | 0.0950 ms | 0.0889 ms |

So why exactly is the List<T> version faster than the IList<T> version ? To answer this, one would have to look behind the foreach statement, which gets translates to following under the hood.

public void ForEachIList(IList<int> collection)
{
    using IEnumerator<int> enumerator = collection.GetEnumerator();
    while (enumerator.MoveNext())
    {
        int item = enumerator.Current;
        DoSomething(item);
    }
}

public void ForEachList(List<int> collection)
{
    using List<int>.Enumerator enumerator = collection.GetEnumerator();
    while (enumerator.MoveNext())
    {
        int item = enumerator.Current;
        DoSomething(item);
    }
}

The interesting point to note here is how the the GetEnumerator method return List<int>.Enumerator in the List implementation. This could be verified when checking the source of List<T>. In fact, it implements 3 version of GetEnumerator (explicit implementation).

public List<T>.Enumerator GetEnumerator() 
{
    return new List<T>.Enumerator(this);
}

IEnumerator<T> IEnumerable<T>.GetEnumerator() 
{
    return new Enumerator(this);
}
 
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
{
    return new Enumerator(this);
}

As you can observe, this returns a struct List<T>.Enumerator when using List<T>, while it a boxed version of List<T>.Enumerator when using IList<T>. There is another advantage. Since the List<T>.Enumerator is struct (and statically typed), you are also unburdened by the overhead of traversing the virtual tables and identifying the method. This provides the extra performance gains for List<T> compared to the interface based iterations.

Having said so, these differences wouldn’t quite be making a huge impact in your normal applications. But when the scenario demands micro optimizations, minor changes like these could make a difference.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s