Поделиться через


To foreach or not to foreach that is the question.

Recently email was forwarded to me with a link to a page with some performance tips for developers.  The second performance tip on the page was:

foreach

foreach through an array is incredibly slow compared to for (int i = 0; i < array.Length; i)

This one leapt out at me because it is well ... not to put too fine a point on it ... wrong!  (at least that is what I thought).  C# and Visual Basic 7 both contain specific optimizations to ensure that  foreach on arrays perform equivalently to for(int i=0; i < array.Length; i); I have written plenty of code assuming foreach and for(int i; ...) were equivalent.  To verify my assumption and to ensure that the code I wrote was as efficient as possible in the future I did a little experiment: I wrote a small C# application with 2 functions the first of which enumerated an array of integers using foreach, the second enumerated an array of integers using for(int ...).  I then used ildasm to disassemble the compiled binary to IL and then examined the resulting code.

Test code:

using System;class MyClass{    static void One()    {        int[] a = new int[10000];        foreach(int i in a)            Console.WriteLine(i);    }    static void Two()    {        int[] a = new int[10000];        for(int j=0; j<a.Length; j++)       {            int i = a[j];            Console.WriteLine(i);        }    }}

I saved this little program in to a c# source file called fe.cs and compiled it using the command line:

csc /t:library fe.cs

The .Net Framework sdk is installed with Visual Studio 7.0 and 7.1 or it can be downloaded from microsoft at: https://msdn.microsoft.com/netframework/downloads/updates/default.aspx .   The SDK contains a nifty utility called ildasm.exe.  If you have Visual Studio 7 or 7.1 installed then you already have the sdk installed.

Ildasm rather cleverly operates as either a command line tool or as a GUI depending.  For my purposes the command line was all that I required.  I created an IL file using the command (you may need to ensure that the path is set to the "Program Files\Microsoft.NET\SDK\v1.1\Bin" directory:

ildasm fe.dll /out:fe.il /text

then in an editor I examined this file's details, the interesting part for me is the method bodies for One (foreach use) and Two(for(int; ..) use.

    static void One()    {        int[] a = new int[10000];        foreach(int i in a)            Console.WriteLine(i);    }   static void Two()    {        int[] a = new int[10000];        for(int j=0; j<a.Length; j++)       {            int i = a[j];            Console.WriteLine(i);        }    }
.method private hidebysig static void One() cil managed{// Code size 38 (0x26).maxstack 2.locals init (int32[] V_0,int32 V_1,int32[] V_2,int32 V_3) .method private hidebysig static void Two() cil managed{// Code size 36 (0x24).maxstack 2.locals init (int32[] V_0,int32 V_1,int32 V_2)
IL_0000: ldc.i4 0x2710IL_0005: newarr [mscorlib]System.Int32IL_000a: stloc.0IL_000b: ldloc.0IL_000c: stloc.2IL_000d: ldc.i4.0IL_000e: stloc.3IL_000f: br.s IL_001f IL_0000: ldc.i4 0x2710IL_0005: newarr [mscorlib]System.Int32IL_000a: stloc.0IL_000b: ldc.i4.0IL_000c: stloc.1IL_000d: br.s IL_001d
IL_0011: ldloc.2IL_0012: ldloc.3IL_0013: ldelem.i4IL_0014: stloc.1IL_0015: ldloc.1IL_0016: call void [mscorlib]System.Console::WriteLine(int32)IL_001b: ldloc.3IL_001c: ldc.i4.1IL_001d: addIL_001e: stloc.3IL_001f: ldloc.3IL_0020: ldloc.2IL_0021: ldlenIL_0022: conv.i4IL_0023: blt.s IL_0011 IL_000f: ldloc.0IL_0010: ldloc.1IL_0011: ldelem.i4IL_0012: stloc.2IL_0013: ldloc.2IL_0014: call void [mscorlib]System.Console::WriteLine(int32)IL_0019: ldloc.1IL_001a: ldc.i4.1IL_001b: addIL_001c: stloc.1IL_001d: ldloc.1IL_001e: ldloc.0IL_001f: ldlenIL_0020: conv.i4IL_0021: blt.s IL_000f
IL_0025: ret} // end of method MyClass::One IL_0023: ret} // end of method MyClass::Two

If you examine the IL for each version of the function you will see that the red is basic variable initialization, creating the array setting loop counters to zero, that kind of thing.  The bold blue text is the body of the loop, close examination shows that they are effectively identical.  Which confirms my original belief that there is no performance benefit to be gained by replacing foreach(...) with for(int i; ...).

There are circumstances where foreach() introduces a performance penalty; but when the compiler can statically determine that the collection is infact an array, then foreach performs exactly the same as the equivalent hand coded loop.

I hope to visit managed collections again in the future.

Comments

  • Anonymous
    April 19, 2004
    I'm glad to hear that's true for arrays, but it certainly is NOT true for all collections. I found that foreach loops over HashTables and ArrayLists were rather "slow" compared to simple for loops, which made a big difference.

  • Anonymous
    April 19, 2004
    Indeed, I hope to spend some time talking about that in a future entry.

  • Anonymous
    April 19, 2004
    That is a huge blanket statement. If you are using an ArrayList, with value types you would have huge perf consequences due to boxing.

    I have heard a similar statement (foreach is slower than for ()) I guess it just depends on the context.

  • Anonymous
    April 19, 2004
    Yup, I posted on Eric Gunnersons blog about what Paul mentions, and I had a long running 'debate' about this on the ASP.NET perf forums. The most useful tool I've found for doing these comparisons (without digging through the IL) is Reflector - the C# output is pretty revealing in these debates.

  • Anonymous
    April 19, 2004
    you are right. Foreach is not necessarily better or worse from a performance point of view.

    But THE most important thing, is that unless you have a specific need to optimize, foreach is just more elegant. If you're only iterating through a few items, who cares either way?

    Use foreach unless you have a very good reason not to.

  • Anonymous
    April 20, 2004
    These are not identical. Get rid of the int i = a[j]; in the second one and ildasm and you save two instructions inside the loop. You can save yet another two instructions by avoiding checking < a.Length (set int len = a.Length at the start, and compare len). I just ildasm'd all three examples myself. The foreach is the worst of all. And this example is the most efficient possible foreach loop, since the compiler optimizes for it and normal cases would require IEnumerable. I personally thing that the while < a.Length is the worst option of the three in the general case, because you have no freekin' clue what is happening in that property access. FX guidelines say to not do heay work in a getter, but I have seen plenty of violations.

  • Anonymous
    April 21, 2004
    Actually, you do not want to set the length into a temporary variable. In a for loop, where the length is set as the end condition of the loop, the IL does some optimizations to stop checking for out of bounds errors, because by defenition you are not out of bounds.

    When you put the length into a temp variable, it must do those checks on each iteration. Brad A. has done an extensive analysis of this issue.

  • Anonymous
    April 22, 2004
    The comment has been removed

  • Anonymous
    June 06, 2005
    Here are some resources to read up on the differences between 'for' and 'foreach'.
    Enumerating array...

  • Anonymous
    August 10, 2006
    PingBack from http://www.netcrucible.com/blog/2004/04/21/loopy-decisions/

  • Anonymous
    August 10, 2006
    PingBack from http://www.netcrucible.com/blog/2004/04/21/loopy-decisions-2/

  • Anonymous
    July 27, 2007
    for和foreach 的效率问题是个老问题了,从网上看到的是众说纷纭,有说for效率高的也有说foreach效率高的,还有说测试方法有问题的;通过实验发现for的效率比foreach高。

  • Anonymous
    August 07, 2007
    事实上对Array进行foreach的时候根本不会调用GetEnumerator()方法, 也就是被编译器给内联了. 而对于List&lt;&gt;, 虽然没有编译器内联GetEnumerator()

  • Anonymous
    July 08, 2008
    Although very handy, C#&#39;s foreach statement is actually quite dangerous. In fact, I may swear off its use entirely. Why? Two reasons: (1) performance, and (2) predictability....