23

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?

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Murdock
  • 4,352
  • 3
  • 34
  • 63
  • 2
    @UnholySheep: That's at least not the *majority* of what's going on here. – Jon Skeet Jan 04 '21 at 21:58
  • 1
    @GSerg: Nope, that's not a dupe of this one either. That refers to iterating over different types - here there's a `List` (and an empty one at that) in both cases. – Jon Skeet Jan 04 '21 at 22:05
  • 1
    @JonSkeet I don't see enumeration of different types being compared there? The [most upvoted answer](https://stackoverflow.com/a/51602520/11683) also looks pretty much like yours. – GSerg Jan 04 '21 at 22:10
  • 1
    @GSerg: Nope: "What we know about List is that it is an in-memory collection, so the MoveNext() function on its enumerator is going to be very cheap. It looks like your collection gives an enumerator whose MoveNext() method is more expensive, perhaps because it is interacting with some external resource such as a database connection." – Jon Skeet Jan 04 '21 at 22:12
  • 3
    @GSerg: In other words, the *actual type of the iterable at execution time* is different in that question, whereas in this case the actual type is `List` in both cases. – Jon Skeet Jan 04 '21 at 22:13
  • @JonSkeet That's a quote from the [accepted answer](https://stackoverflow.com/a/23536909/11683), not the [most upvoted](https://stackoverflow.com/a/51602520/11683)? Although yes, I do now see your point about different types. Still, IMO the most upvoted answer applies? – GSerg Jan 04 '21 at 22:15
  • 2
    @GSerg: Ah, yes, I see what you mean now; sorry for not looking at the right answer. But I'd still say it's not a duplicate *question* - that most upvoted answer actually looks to be irrelevant to the question being asked, IMO. It states facts, but they're not the important ones for *that* question, whereas they're the important ones on *this* question. – Jon Skeet Jan 04 '21 at 22:17

1 Answers1

41

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.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    @00110001: No, that's not the difference. That wouldn't affect allocation. The difference is in whether the iterator is boxed or not. – Jon Skeet Jan 04 '21 at 23:14
  • 2
    I failed the quiz in the [blog post](https://codeblog.jonskeet.uk/2010/07/27/iterate-damn-you/ "Iterate, damn you!"). :-) – Theodor Zoulias Jan 04 '21 at 23:30
  • I use `IEnumerable` for parameters to indicate that the collection is read only and can't be added to/removed from (though it's not good enough to stop people from modifying objects in the collection...) – ldam Jan 11 '21 at 18:49
  • @ldam: And that's almost always fine - but it does come with a performance penalty, as abstraction often does. – Jon Skeet Jan 11 '21 at 22:52