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.