IEnumerable performance cost

Exploring the cost of virtualization with IEnumerable

Home DailyDrop

Daily Knowledge Drop

Using IEnumerable<> instead of a concrete implementation (List or an array for example) can make the code more usable and concise, but comes with a performance cost.

If performance is critical, it will be worth performing a type check and handling accordingly.


Generic method

We want to create a method which takes a collection of integers, sums them up and returns the result. We are not entirely sure what this collection will be (a list, an array) so we want to make the method generic.

We might end up with something like this:

public int SumItems(IEnumerable<int> enumerable)
{
    var runningSum = 0;

    foreach(var item in enumerable)
    {
        runningSum += item;
    }

    return runningSum;
}

This is fairly straightforward - the method takes an IEnumerable<int> as a parameter, will iterate through each item keeping a running total, and then return the total at the end.

Lets look at the performance of this method using various implementations of IEnumerable<int>.

IEnumerable<int> enumerableData = Enumerable.Range(0, 10000);
IEnumerable<int> listData = new List<int>(Enumerable.Range(0, 10000));
IEnumerable<int> arrayData = Enumerable.Range(0, 10000).ToArray();

Here three different implementations are declared and populated with 10000 items. We have:

  • an IEnumerable<int>
  • a List<int>
  • an int array

As all three of these implement IEnumerable, they can all be passed to the SumItems method as a parameter.

Using BenchmarkDotNet our method is called using each of the IEnumerable types:

Method IEnumerable Mean Error StdDev
SumItems IEnumerable 37.400 us 0.4611 us 0.4313 us
SumItems List 57.201 us 0.4353 us 0.3635 us
SumItems int[] 37.226 us 0.3353 us 0.2972 us

As one can see, the IEnumerable and the int array are comparable, with the List considerably slower (relatively).


Type checking

Next, let's modify the SumItems method to check the type of IEnumerable, cast to that type, and then iterate on the specific type instead of IEnumerable.

This could have also been done with method overloading, but with this approach all the code is kept in a single method:

public int SumListChecked(IEnumerable<int> enumerable)
{
    var runningSum = 0;

    // check for List
    if(enumerable is List<int> list)
    {
        foreach (var item in list)
        {
            runningSum += item;
        }
        return runningSum;
    }

    // check for array
    if (enumerable is int[] array)
    {
        foreach (var item in array)
        {
            runningSum += item;
        }
        return runningSum;
    }

    // all others
    foreach (var item in enumerable)
    {
        runningSum += item;
    }

    return runningSum;
}

Here, the type of IEnumerable parameter enumerable is checked, if its a List or Array, then the IEnumerable is cast to that type, and the foreach loop is done on the cast type not on the original IEnumerable.

Running the benchmarks for both methods, yields the following results:

Method IEnumerable Mean Error StdDev
SumItems IEnumerable 37.400 us 0.4611 us 0.4313 us
SumListChecked IEnumerable 34.846 us 0.3551 us 0.3148 us
SumItems List 57.201 us 0.4353 us 0.3635 us
SumListChecked List 7.523 us 0.0806 us 0.0754 us
SumItems int[] 37.226 us 0.3353 us 0.2972 us
SumListChecked int[] 3.580 us 0.0213 us 0.0189 us

IEnumerable is effectively the same, as the code is the same between the two method, however List and Array are considerably quicker when performing the iterating on the concrete type and not IEnumerable.

Notes

While this type of micro-optimization will probably not be required or noticeable in most use cases, its worth knowing about the trade-offs which come with each approach.

  • Just using IEnumerable: simplest, most maintainable code, but slowest in most cases
  • Type checking: less maintainable, but better performance in some cases
  • Method overloading: even less maintainable, as the same/similar code will be in multiple methods but better performance in some cases

References

The cost of virtualization in .NET can be surprising

Daily Drop 89: 06-06-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net ienumerable performance