Here's an interesting question - having an array of integers, is it faster to enumerate it, to perform a simple operation on an element, with for
or foreach
?
To answer this, first of all I've created a benchmark that covers 3 use cases:
- Use
for
loop and get an element by it's index. - Use
foreach
on the array. - Use
foreach
on the array, but before enumerating case the array toIEnumerable
forcibly. I don't know why I had a hunch that might behave slightly different, but I did it anyway.
Here is the testing code:
#LINQPad optimize+
void Main()
{
Util.AutoScrollResults = true;
BenchmarkRunner.Run<Enumeration>();
}
[ShortRunJob]
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
[MemoryDiagnoser]
[MarkdownExporter]
public class Enumeration
{
int[] _source;
[Params(10, 100, 1000, 10000, 100000)]
public int Length;
[GlobalSetup]
public void Setup()
{
_source = Enumerable.Range(0, Length).ToArray();
}
[Benchmark]
public void EnumerateAsArray()
{
int count = 0;
for(int i = 0; i < Length; i++) {
int element = _source[i];
count += 1;
}
}
[Benchmark]
public void EnumerateWithForeach()
{
int count = 0;
foreach(int i in _source) {
count += 1;
}
}
[Benchmark]
public void EnumerateWithForeachAfterCasting() {
int count = 0;
foreach (int i in (IEnumerable)_source) {
count += 1;
}
}
}
And the result:
Method | Length | Mean | Error | StdDev | Min | Max | Median | Gen0 | Allocated |
---|---|---|---|---|---|---|---|---|---|
EnumerateAsArray | 10 | 3.473 ns | 1.4879 ns | 0.0816 ns | 3.385 ns | 3.546 ns | 3.490 ns | - | - |
EnumerateWithForeach | 10 | 3.081 ns | 0.6979 ns | 0.0383 ns | 3.057 ns | 3.125 ns | 3.062 ns | - | - |
EnumerateWithForeachAfterCasting | 10 | 360.151 ns | 112.8064 ns | 6.1833 ns | 353.018 ns | 363.983 ns | 363.452 ns | 0.0648 | 272 B |
EnumerateAsArray | 100 | 49.681 ns | 10.2353 ns | 0.5610 ns | 49.237 ns | 50.311 ns | 49.495 ns | - | - |
EnumerateWithForeach | 100 | 42.489 ns | 15.0294 ns | 0.8238 ns | 41.728 ns | 43.364 ns | 42.375 ns | - | - |
EnumerateWithForeachAfterCasting | 100 | 3,365.735 ns | 1,017.7344 ns | 55.7855 ns | 3,330.609 ns | 3,430.059 ns | 3,336.535 ns | 0.5798 | 2432 B |
EnumerateAsArray | 1000 | 404.754 ns | 101.6160 ns | 5.5699 ns | 399.244 ns | 410.382 ns | 404.638 ns | - | - |
EnumerateWithForeach | 1000 | 366.522 ns | 44.1604 ns | 2.4206 ns | 364.396 ns | 369.157 ns | 366.015 ns | - | - |
EnumerateWithForeachAfterCasting | 1000 | 34,436.210 ns | 16,553.4298 ns | 907.3493 ns | 33,513.147 ns | 35,326.984 ns | 34,468.500 ns | 5.7373 | 24032 B |
EnumerateAsArray | 10000 | 4,011.964 ns | 1,402.6901 ns | 76.8862 ns | 3,924.527 ns | 4,069.007 ns | 4,042.358 ns | - | - |
EnumerateWithForeach | 10000 | 3,644.260 ns | 350.8573 ns | 19.2317 ns | 3,632.706 ns | 3,666.461 ns | 3,633.613 ns | - | - |
EnumerateWithForeachAfterCasting | 10000 | 340,725.798 ns | 170,058.5539 ns | 9,321.4832 ns | 330,179.199 ns | 347,861.084 ns | 344,137.109 ns | 57.1289 | 240033 B |
EnumerateAsArray | 100000 | 40,460.423 ns | 9,494.4089 ns | 520.4206 ns | 40,090.979 ns | 41,055.597 ns | 40,234.692 ns | - | - |
EnumerateWithForeach | 100000 | 36,107.100 ns | 6,022.3198 ns | 330.1037 ns | 35,901.172 ns | 36,487.848 ns | 35,932.281 ns | - | - |
EnumerateWithForeachAfterCasting | 100000 | 3,892,458.333 ns | 4,019,109.7373 ns | 220,300.9666 ns | 3,652,532.422 ns | 4,085,627.734 ns | 3,939,214.844 ns | 570.3125 | 2400038 B |
The findings are impressive - enumerating an array with for
or foreach
is not much different, but surprisingly foreach
is slightly faster (around 12%). Whereas when casting to IEnumerable
and then performing iteration is way way slower than anything else! In fact, it's about 107 times slower than normal array iteration!
To understand what's happening, I'm going to disassembly the generated code.
EnumerateAsArray
This is IL representation:
IL_0000 ldc.i4.0
IL_0001 stloc.0 // count
IL_0002 ldc.i4.0
IL_0003 stloc.1 // i
IL_0004 br.s IL_0017
IL_0006 ldarg.0
IL_0007 ldfld Enumeration._source
IL_000C ldloc.1 // i
IL_000D ldelem.i4
IL_000E pop
IL_000F ldloc.0 // count
IL_0010 ldc.i4.1
IL_0011 add
IL_0012 stloc.0 // count
IL_0013 ldloc.1 // i
IL_0014 ldc.i4.1
IL_0015 add
IL_0016 stloc.1 // i
IL_0017 ldloc.1 // i
IL_0018 ldarg.0
IL_0019 ldfld Enumeration.Length
IL_001E blt.s IL_0006
IL_0020 ret
As you can see, this is what's happening:
- Create two variables -
count
andi
. - Keep comparing array length to
i
(IL_0017). Wheni
is less than array length, jump to IL_0006. - Load array element.
- Increment the count.
- Increment
i
.
Simple enough.
EnumerateWithForeach
IL source:
IL_0000 ldc.i4.0
IL_0001 stloc.0 // count
IL_0002 ldarg.0
IL_0003 ldfld Enumeration._source
IL_0008 stloc.1
IL_0009 ldc.i4.0
IL_000A stloc.2
IL_000B br.s IL_0019
IL_000D ldloc.1
IL_000E ldloc.2
IL_000F ldelem.i4
IL_0010 pop
IL_0011 ldloc.0 // count
IL_0012 ldc.i4.1
IL_0013 add
IL_0014 stloc.0 // count
IL_0015 ldloc.2
IL_0016 ldc.i4.1
IL_0017 add
IL_0018 stloc.2
IL_0019 ldloc.2
IL_001A ldloc.1
IL_001B ldlen
IL_001C conv.i4
IL_001D blt.s IL_000D
IL_001F ret
It's not much different to before, but .NET runtime makes slight optimisation, because it knows you are going to need to value and not need the index (foreach
always retrieves the value). So that's good!
EnumerateWithForeachAfterCasting
IL source:
IL_0000 ldc.i4.0
IL_0001 stloc.0 // count
IL_0002 ldarg.0
IL_0003 ldfld Enumeration._source
IL_0008 callvirt IEnumerable.GetEnumerator ()
IL_000D stloc.1
IL_000E br.s IL_0020
IL_0010 ldloc.1
IL_0011 callvirt IEnumerator.get_Current ()
IL_0016 unbox.any Int32
IL_001B pop
IL_001C ldloc.0 // count
IL_001D ldc.i4.1
IL_001E add
IL_001F stloc.0 // count
IL_0020 ldloc.1
IL_0021 callvirt IEnumerator.MoveNext ()
IL_0026 brtrue.s IL_0010
IL_0028 leave.s IL_003B
IL_002A ldloc.1
IL_002B isinst IDisposable
IL_0030 stloc.2
IL_0031 ldloc.2
IL_0032 brfalse.s IL_003A
IL_0034 ldloc.2
IL_0035 callvirt IDisposable.Dispose ()
IL_003A endfinally
IL_003B ret
Wow! You don't need to be an IL expect to see this will be slow, because it's full of callvirt
calls. Calling virtual methods is expensive! This time you are forcing C# to use IEnumerable
pattern, and that's exactly what it does. To represent this code in C# 1, it translates to the following:
int count = 0;
IEnumerator enumerator = ((IEnumerable)_source).GetEnumerator ();
try
{
while (enumerator.MoveNext ())
{
int num = (int)enumerator.Current;
count++;
}
}
finally
{
IDisposable disposable = enumerator as IDisposable;
if (disposable != null)
{
disposable.Dispose ();
}
}
So a lot of method calls and unboxing, all are extremely expensive.
Summary
- If you can, prefer
foreach
tofor
loops, they will be slightly faster. - Never cast arrays to
IEnumerable
and then enumerate, as this expands into a lot of virtual calls usingIEnumerator
pattern.
Top comments (0)