During my recent browsing on LinkedIn I saw that question: IEnumerable vs IEnumerator in C#: One is 2x Faster – Which One?
Naturally, I was very suspicious. So let's find out what is going on here.
The code
Let's start with the code in question:
public class EnumerableVsEnuemrator
{
private List<decimal> _productPrices = [19.99m, 34.50m, 50m, 12.75m, 99.99m, 14.30m, 5.99m, 20.15m];
[Benchmark]
public void TestIEnumerable()
{
IEnumerable<decimal> priceCollection = _productPrices;
foreach (var price in priceCollection)
{
Debug.WriteLine(price);
}
}
[Benchmark]
public void TestIEnumerator()
{
IEnumerator<decimal> priceEnumerator = _productPrices.GetEnumerator();
while (priceEnumerator.MoveNext())
{
Debug.WriteLine(priceEnumerator.Current);
}
}
public static void Main() => BenchmarkRunner.Run<EnumerableVsEnuemrator>();
}
So the same list will be taken for the enumeration, but once with IEnumerable
and once with IEnumerator
. So what is the difference between those two?
IEnumerable vs IEnumerator
In very simple terms: IEnumerable
tells you that something is enumerable. Like, you have a basket of apples. But how to go from one apple to the next? That's where IEnumerator
comes in. The enumerator describes how to go from one apple to the next (or when you are done).
Spoiler-alert: IEnumerable
is defined as (simplified):
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
So, IEnumerable
is just a wrapper around IEnumerator
. So, why then the 2x times difference in speed?
The problem with the benchmark
The whole code has three major problems:
- Problem 1: The
Debug.WriteLine
is a poor choice of "Act". Mainly because it is backed by theConditionalAttribute
. - Problem 2: The sample size of 8 elements is way too low. Like way way way too low.
- Problem 3: No return value. The act (the
Debug.WriteLine
) is not returning anything. So the compiler can optimize the whole loop away.
So all in all, those two pieces of code are not comparable. And by the way, on my machine, the results look like this:
| Method | Mean | Error | StdDev |
|---------------- |---------:|---------:|---------:|
| TestIEnumerable | 16.90 ns | 0.044 ns | 0.034 ns |
| TestIEnumerator | 11.15 ns | 0.095 ns | 0.084 ns |
It is always important to measure for your specific problem (and that also includes platform and so on). Anyway, let's go through the problems one by one.
Problem 1: Debug.WriteLine
If we have a look at the Debug.WriteLine
method, we see that it is backed by the ConditionalAttribute
.
public static class Debug
{
[Conditional("DEBUG")]
public static void WriteLine(string message, params object?[] args) { ... }
}
The important bid here is two-folded:
If you use the ConditionalAttribute
on a method with "DEBUG", the compiler will remove the content of your method if the DEBUG
symbol is not defined. So, in a release build, the whole Debug.WriteLine
body will be removed. Therefore you have an empty method. Even if that wouldn't be the case, Debug.WriteLine
is a method with side-effects (like writing to the console). This is not a good choice for a benchmark as it has a big influence on the results.
Problem 2: Sample size
The sample size is way too low. Only having a few elements will not be sufficient to filter out noise! To make that somewhat useful, take at least 10000 elements.
Problem 3: No return value
The act (the Debug.WriteLine
) is not returning anything and paired with Problem 1 the compiler can optimize the whole loop away. So, the whole benchmark is not doing anything and therefore those two results are not comparable!
Lowered Code
And yes, Problem 3 is the realitiy here:
public void TestIEnumerable()
{
IEnumerator<decimal> enumerator = ((IEnumerable<decimal>)_productPrices).GetEnumerator();
try
{
while (enumerator.MoveNext())
{
decimal current = enumerator.Current;
}
}
finally
{
if (enumerator != null)
{
enumerator.Dispose();
}
}
}
public void TestIEnumerator()
{
IEnumerator<decimal> enumerator = _productPrices.GetEnumerator();
while (enumerator.MoveNext())
{
}
}
Source on Sharlab.io
The whole body is getting removed! So, the whole benchmark is not doing anything and therefore those two results are not comparable!
Let's fix it
Let's address all the issues and make a proper benchmark out of it.
public class EnumerableVsEnuemrator
{
private List<decimal> _productPrices = Enumerable.Range(0, 10_000)
.Select(i => (decimal)i)
.ToList();
[Benchmark]
public decimal TestIEnumerable()
{
IEnumerable<decimal> priceCollection = _productPrices;
var sum = 0m;
foreach (var price in priceCollection)
{
sum += price;
}
return sum;
}
[Benchmark]
public decimal TestIEnumerator()
{
IEnumerator<decimal> priceEnumerator = _productPrices.GetEnumerator();
var sum = 0m;
while (priceEnumerator.MoveNext())
{
sum += priceEnumerator.Current;
}
return sum;
}
public static void Main() => BenchmarkRunner.Run<EnumerableVsEnuemrator>();
}
With the following result:
| Method | Mean | Error | StdDev |
|---------------- |---------:|---------:|---------:|
| TestIEnumerable | 58.12 us | 0.647 us | 0.605 us |
| TestIEnumerator | 51.97 us | 0.308 us | 0.257 us |
Not so flashy after all.It is a bit slower due to the overhead of the foreach
loop (and virtual method calls).
Therefore: Always measure for your specific problem and don't trust everything blindly you read on the internet.