I found List.ForEach to be significantly faster. Here are the results of the last four runs of the (now revised) performance test:
NativeForLoop: 00:00:04.7000000
ListDotForEach: 00:00:02.7160000
---------------------------------------
NativeForLoop: 00:00:04.8660000
ListDotForEach: 00:00:02.6560000
---------------------------------------
NativeForLoop: 00:00:04.6240000
ListDotForEach: 00:00:02.8160000
---------------------------------------
NativeForLoop: 00:00:04.7110000
ListDotForEach: 00:00:02.7190000
Each test was executed with one hundred million (100,000,000) iterations. I updated the test to use a custom class (Fruit) and have each loop access and work with a member inside the current object. Each loop is performing the same task.
Here is the entire source of the test class:
class ForEachVsClass
{
static Int32 Iterations = 1000000000;
static int Work = 0;
public static void Init(string[] args)
{
if (args.Length > 0)
Iterations = Int32.Parse(args[0]);
Console.WriteLine("Iterations: " + Iterations);
}
static List<Fruit> ListOfFruit = new List<Fruit> {
new Fruit("Apple",1), new Fruit("Orange",2), new Fruit("Kiwi",3), new Fruit("Banana",4) };
internal class Fruit {
public string Name { get; set; }
public int Value { get; set; }
public Fruit(string _Name, int _Value) { Name = _Name; Value = _Value; }
}
[Benchmark]
public static void NativeForLoop()
{
for (int x = 0; x < Iterations; x++)
{
NativeForLoopWork();
}
}
public static void NativeForLoopWork()
{
foreach (Fruit CurrentFruit in ListOfFruit) {
Work+=CurrentFruit.Value;
}
}
[Benchmark]
public static void ListDotForEach()
{
for (int x = 0; x < Iterations; x++)
{
ListDotForEachWork();
}
}
public static void ListDotForEachWork()
{
ListOfFruit.ForEach((f)=>Work+=f.Value);
}
}
Here is the resulting IL for the work methods (extracted to make them easier to read):
.method public hidebysig static void NativeForLoopWork() cil managed
{
.maxstack 2
.locals init (
[0] class ForEachVsClass/Fruit CurrentFruit,
[1] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ForEachVsClass/Fruit> CS$5$0000)
L_0000: ldsfld class [mscorlib]System.Collections.Generic.List`1<class ForEachVsClass/Fruit> ForEachVsClass::ListOfFruit
L_0005: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> [mscorlib]System.Collections.Generic.List`1<class ForEachVsClass/Fruit>::GetEnumerator()
L_000a: stloc.1
L_000b: br.s L_0026
L_000d: ldloca.s CS$5$0000
L_000f: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator<class ForEachVsClass/Fruit>::get_Current()
L_0014: stloc.0
L_0015: ldsfld int32 ForEachVsClass::Work
L_001a: ldloc.0
L_001b: callvirt instance int32 ForEachVsClass/Fruit::get_Value()
L_0020: add
L_0021: stsfld int32 ForEachVsClass::Work
L_0026: ldloca.s CS$5$0000
L_0028: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator<class ForEachVsClass/Fruit>::MoveNext()
L_002d: brtrue.s L_000d
L_002f: leave.s L_003f
L_0031: ldloca.s CS$5$0000
L_0033: constrained [mscorlib]System.Collections.Generic.List`1/Enumerator<class ForEachVsClass/Fruit>
L_0039: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_003e: endfinally
L_003f: ret
.try L_000b to L_0031 finally handler L_0031 to L_003f
}
.method public hidebysig static void ListDotForEachWork() cil managed
{
.maxstack 8
L_0000: ldsfld class [mscorlib]System.Collections.Generic.List`1<class ForEachVsClass/Fruit> ForEachVsClass::ListOfFruit
L_0005: ldsfld class [mscorlib]System.Action`1<class ForEachVsClass/Fruit> ForEachVsClass::CS$<>9__CachedAnonymousMethodDelegate1
L_000a: brtrue.s L_001d
L_000c: ldnull
L_000d: ldftn void ForEachVsClass::<ListDotForEachWork>b__0(class ForEachVsClass/Fruit)
L_0013: newobj instance void [mscorlib]System.Action`1<class ForEachVsClass/Fruit>::.ctor(object, native int)
L_0018: stsfld class [mscorlib]System.Action`1<class ForEachVsClass/Fruit> ForEachVsClass::CS$<>9__CachedAnonymousMethodDelegate1
L_001d: ldsfld class [mscorlib]System.Action`1<class ForEachVsClass/Fruit> ForEachVsClass::CS$<>9__CachedAnonymousMethodDelegate1
L_0022: callvirt instance void [mscorlib]System.Collections.Generic.List`1<class ForEachVsClass/Fruit>::ForEach(class [mscorlib]System.Action`1<!0>)
L_0027: ret
}