Nerdy tidbits from my life as a software engineer

Thursday, March 26, 2009

Foreach vs For Statements

Have you ever looked at a foreach clause in ILDasm before?  It’s very interesting, and probably not what you expect.  Take the following simple line of code:

List<MethodExpectation> mMyList = new List<MethodExpectation>();
foreach(MethodExpectation expectation in mMyList)

Here’s what the compiler spits out for the foreach statement:

IL_0158:  ldarg.0
IL_0159:  ldfld      class [mscorlib]System.Collections.Generic.List`1<class [MyAssembly]MethodExpectation> MyClass::mMyList
IL_015e:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<class [MyAssembly]MethodExpectation>::GetEnumerator()
IL_0163:  stloc.s    CS$5$0003
  IL_0165:  br.s       IL_0171
  IL_0167:  ldloca.s   CS$5$0003
  IL_0169:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [MyAssembly]MethodExpectation>::get_Current()
  IL_016e:  stloc.2
  IL_0171:  ldloca.s   CS$5$0003
  IL_0173:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [MyAssembly]MethodExpectation>::MoveNext()
  IL_0178:  stloc.s    CS$4$0001
  IL_017a:  ldloc.s    CS$4$0001
  IL_017c:  brtrue.s   IL_0167
  IL_017e:  leave.s    IL_018f
}  // end .try
  IL_0180:  ldloca.s   CS$5$0003
  IL_0182:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [MyAssembly]MethodExpectation>
  IL_0188:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
  IL_018e:  endfinally
}  // end handler

I suppose I always imagined that using IEnumerable to iterate through a list was less efficient than using a simple for loop.  But compared to the following, I suppose it’s hard to tell:

List<MethodExpectation> mMyList = new List<MethodExpectation>();
for(int i = 0; i < mMyList.Count; i++)
  MethodExpectation methodExpectation = mMyList[i];

You can see the difference fairly clearly:

IL_0156:  ldc.i4.0
IL_0157:  stloc.2
IL_0158:  br.s       IL_016d
IL_015a:  nop
IL_015b:  ldarg.0
IL_015c:  ldfld      class [mscorlib]System.Collections.Generic.List`1<class [MyAssembly]MethodExpectation> MyClass::mMyList
IL_0161:  ldloc.2
IL_0162:  callvirt   instance !0 class [mscorlib]System.Collections.Generic.List`1<class [MyAssembly]MethodExpectation>::get_Item(int32)
IL_0167:  stloc.3
IL_0168:  nop
IL_0169:  ldloc.2
IL_016a:  ldc.i4.1
IL_016b:  add
IL_016c:  stloc.2
IL_016d:  ldloc.2
IL_016e:  ldarg.0
IL_016f:  ldfld      class [mscorlib]System.Collections.Generic.List`1<class [MyAssembly]MethodExpectation> MyClass::mMyList
IL_0174:  callvirt   instance int32 class [mscorlib]System.Collections.Generic.List`1<class [MyAssembly]MethodExpectation>::get_Count()
IL_0179:  clt
IL_017b:  stloc.s    CS$4$0001
IL_017d:  ldloc.s    CS$4$0001
IL_017f:  brtrue.s   IL_015a

This is a pretty standard for loop.  The top line sets the i variable (stored in loc2 / address CS$4$001) to 0 and breaks to 016d, which does the less than check.  If the check passes it breaks to the inside of the loop, which starts at instruction 015a (another strange nop).  The increment of i happens at the end of the loop right before the less than check occurs again.

Contrast this to using the enumerator, which is actually fairly clean considering what it’s doing.  The main thing that I wonder about is what kind of performance hit you get by setting up a try / finally block.  I actually don’t know the answer to this, but I always assumed that you get quite a big performance hit when you create an exception frame.  If that’s the case then using foreach instead of for would clearly be less desirable from a performance standpoint.

I had always figured that foreach’s would be slower since you were newing objects on the heap (via GetEnumerator()) just so you could go through a simple loop, which is obviously more work than putting an integer on the stack.  But if you add a try / finally block to that as well?  One solution might end up being so much faster than the other that, in a large program, it might end up making a very large difference.

Then again, foreach statements are very convenient.  And if that bothers you, you probably don’t want to look at the code that lambda expressions generate…