In general I tend to use IEnumerable<>
as the type when I pass in parameters. However according to BenchmarkDotNet:
[Benchmark]
public void EnumeratingCollectionsBad()
{
var list = new List<string>();
for (int i = 0; i < 1000; i++)
{
Bad(list);
}
}
[Benchmark]
public void EnumeratingCollectionsFixed()
{
var list = new List<string>();
for (int i = 0; i < 1000; i++)
{
Fixed(list);
}
}
private static void Bad(IEnumerable<string> list)
{
foreach (var item in list)
{
}
}
private static void Fixed(List<string> list)
{
foreach (var item in list)
{
}
}
Method | Job | Runtime | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|---|---|
EnumeratingCollectionsBad | .NET Core 3.1 | .NET Core 3.1 | 17.802 us | 0.3670 us | 1.0764 us | 17.338 us | 6.3782 | - | - | 40032 B |
EnumeratingCollectionsFixed | .NET Core 3.1 | .NET Core 3.1 | 5.015 us | 0.1003 us | 0.2535 us | 4.860 us | - | - | - | 32 B |
Why would the interface version be so much slower (and memory intensive) than the concrete version?
Why would the interface version be so much slower (and memory intensive) than the concrete version?
When it uses the interface, the iteration has to allocate an object on the heap... whereas List<T>.GetEnumerator()
returns a List<T>.Enumerator
, which is a struct, and doesn't require any additional allocation. List<T>.Enumerator
implements IEnumerator<T>
, but because the compiler knows about the concrete type directly, it doesn't need to be boxed.
So even though both methods are operating on an object of the same type (a List<T>
) one calls this method:
IEnumerator<T> GetEnumerator()
... and one calls this:
List<T>.Enumerator GetEnumerator()
The first almost certainly just delegates to the second, but has to box the result because IEnumerator<T>
is a reference type.
The fact that List<T>.GetEnumerator()
returns a mutable struct can have some surprising consequences but it's designed precisely to have the performance benefit you're seeing here.
The use of an interface vs a concrete type can itself have some very minor performance penalties, but the primary cause here is the difference in allocation.