I had a little dispute (which was very close to holy war:) ) with my colleage, about the performance of access to list via indeces VS via enumerator. To operat
If you look at the IL for both versions, you will see that the first version uses an iterator of type System.Collections.Generic.List<System.Int32>+Enumerator
-- a nested struct
, which is optimized for iterating over a list.
The second version uses a generic implementation of System.Collections.Generic.IEnumerator<System.Int32>
, which is less efficient because it does not "cheat" by keeping a private index to the current item in the list.
I suspect there is a performance gain in employing for instead of foreach (at least for primitive types). As far as I know they are nearly equivalent if you perform for and foreach over the same array (not any other structure like lists, this creates some overheads by itself).
The performance of foreach and for depends on which type of structure you are running for and foreach.
Please check; For and Foreach comparison
When using List<T>
, the foreach
doesn't actually use the IEnumerable<T>
interface; rather, it uses List<T>.Enumerator
, which is a struct
. At the trivial level, this means slightly less indirection - not having to de-reference, and using static calls rather than virtual calls - and a more direct implementation.
These differences are very very small, and in any sensible real-life example the difference is noise. However, it may be marginally noticeable if testing just the foreach
performance.
To expand on this: foreach
doesn't actually require IEnumerable[<T>]
- it can work purely on the GetEnumerator()
/ .MoveNext()
/ .Current
/ .Dispose()
pattern; this was especially important before generics in 2.0.
However, this is only possible when the variable is typed as List<T>
(which has a custom GetEnumerator()
method). Once you have IEnumerable<T>
, it has to use IEnumerator<T>
You can see the code here:
static void Main()
{
List<int> list = new List<int>(Enumerable.Range(1,10000));
int total = 0;
foreach (var i in list)
{
total += i;
}
IEnumerable<int> enumerable = list;
foreach (var i in enumerable)
{
total += i;
}
Console.ReadLine();
}
Which generates this IL. Notice the difference between
System.Collections.Generic.List`1/Enumerator<int32>
and
System.Collections.Generic.IEnumerable`1<int32>
and note that it is a ValueType
(struct):
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 146 (0x92)
.maxstack 2
.locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> list,
[1] int32 total,
[2] int32 i,
[3] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> enumerable,
[4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0000,
[5] bool CS$4$0001,
[6] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> CS$5$0002)
IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: ldc.i4 0x2710
IL_0007: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
int32)
IL_000c: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor(class [mscorlib]System.Collections.Generic.IEnumerable`1<!0>)
IL_0011: stloc.0
IL_0012: ldc.i4.0
IL_0013: stloc.1
IL_0014: nop
IL_0015: ldloc.0
IL_0016: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
IL_001b: stloc.s CS$5$0000
.try
{
IL_001d: br.s IL_002d
IL_001f: ldloca.s CS$5$0000
IL_0021: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_0026: stloc.2
IL_0027: nop
IL_0028: ldloc.1
IL_0029: ldloc.2
IL_002a: add
IL_002b: stloc.1
IL_002c: nop
IL_002d: ldloca.s CS$5$0000
IL_002f: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
IL_0034: stloc.s CS$4$0001
IL_0036: ldloc.s CS$4$0001
IL_0038: brtrue.s IL_001f
IL_003a: leave.s IL_004b
} // end .try
finally
{
IL_003c: ldloca.s CS$5$0000
IL_003e: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
IL_0044: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0049: nop
IL_004a: endfinally
} // end handler
IL_004b: nop
IL_004c: ldloc.0
IL_004d: stloc.3
IL_004e: nop
IL_004f: ldloc.3
IL_0050: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
IL_0055: stloc.s CS$5$0002
.try
{
IL_0057: br.s IL_0067
IL_0059: ldloc.s CS$5$0002
IL_005b: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
IL_0060: stloc.2
IL_0061: nop
IL_0062: ldloc.1
IL_0063: ldloc.2
IL_0064: add
IL_0065: stloc.1
IL_0066: nop
IL_0067: ldloc.s CS$5$0002
IL_0069: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_006e: stloc.s CS$4$0001
IL_0070: ldloc.s CS$4$0001
IL_0072: brtrue.s IL_0059
IL_0074: leave.s IL_008a
} // end .try
finally
{
IL_0076: ldloc.s CS$5$0002
IL_0078: ldnull
IL_0079: ceq
IL_007b: stloc.s CS$4$0001
IL_007d: ldloc.s CS$4$0001
IL_007f: brtrue.s IL_0089
IL_0081: ldloc.s CS$5$0002
IL_0083: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0088: nop
IL_0089: endfinally
} // end handler
IL_008a: nop
IL_008b: call string [mscorlib]System.Console::ReadLine()
IL_0090: pop
IL_0091: ret
} // end of method Program2::Main