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 forloop and get an element by it's index.
- Use foreachon the array.
- Use foreachon the array, but before enumerating case the array toIEnumerableforcibly. 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 - countandi.
- Keep comparing array length to i(IL_0017). Wheniis 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 foreachtoforloops, they will be slightly faster.
- Never cast arrays to IEnumerableand then enumerate, as this expands into a lot of virtual calls usingIEnumeratorpattern.
 


 
    
Top comments (0)