Intro

Let's speak about code optimization. It is a real fun making the code run faster. Just imagine: you take a cup of tea, sit down in front of a code block and speed it up.
How many things in this world can be more satisfying?

Recently I spent a good bit of time optimizing our customer's code. In that code we found a simple loop:

  xFilters.ToList().ForEach(n =>
            {
                XmlNodeList nodesToDelete = xml.SelectNodes(BuildXPath(n.EntityName));
                foreach (XmlNode node in nodesToDelete)
                    node.ParentNode.RemoveChild(node);
            });

My colleague told me something like:

Let's change .ForEach() to regular foreach. Not a big optimization but foreach should be faster.

Surprisingly the method began to work slower after our "optimization"!

So after a couple minutes of thinking I've built a little sample console application to test this behavior in depth.

Stop! Show me the code!

First of all lets create a simple test class to create objects that we will store in our collections.

    public class User
    {
        public string Name { get; set; }
        public Tuple<int, string> Info { get; set; }
        public User(string name, int age, string hobby)
        {
            this.Name = name;
            this.Info = new Tuple<int, string>(age, hobby);
        }
    }
       static double ArrayForeach(User[] array)
        {
            int sum = 0;
            var watch = Stopwatch.StartNew();
            foreach (var item in array)
            {
                sum += item.Info.Item1;
            }
            watch.Stop();
            return watch.Elapsed.TotalMilliseconds;
        }

        static double ArrayFor(User[] array)
        {
            int sum = 0;
            var watch = Stopwatch.StartNew();
            int arrLength = array.Length;
            for (int i = 0; i < arrLength; i++)
            {
                sum += array[i].Info.Item1;
            }
            watch.Stop();
            return watch.Elapsed.TotalMilliseconds;
        }

        static double ArrayForEach(User[] array)
        {
            int sum = 0;
            var watch = Stopwatch.StartNew();
            Array.ForEach(array, i => { sum += i.Info.Item1; });
            watch.Stop();
            return watch.Elapsed.TotalMilliseconds;
        }

        static double ListForeach(List<User> list)
        {
            int sum = 0;
            var watch = Stopwatch.StartNew();
            foreach (var item in list)
            {
                sum += item.Info.Item1;
            }
            watch.Stop();
            return watch.Elapsed.TotalMilliseconds;
        }

        static double ListForEach(List<User> list)
        {
            int sum = 0;
            var watch = Stopwatch.StartNew();
            list.ForEach(i => { sum += i.Info.Item1; });
            watch.Stop();
            return watch.Elapsed.TotalMilliseconds;
        }

        static double ListFor(List<User> list)
        {
            int sum = 0;
            var watch = Stopwatch.StartNew();
            int length = list.Count;
            for (int i = 0; i < length; i++)
            {
                sum += list[i].Info.Item1;
            }
            watch.Stop();
            return watch.Elapsed.TotalMilliseconds;
        }

So we need to make effective time calculation. That's why we should call all methods in cycles, calculate average and print the result after.

 int cycleCount = 1000;
 double sum = 0.0;
 for (int i = 0; i < cycleCount; i++)
 {
     sum += ArrayForeach(arr);
  }
 arrayAverageForeachTime = sum / cycleCount;

And same code for all other methods.

Result

Lets make two charts. One for array and one for list.

Array

List

Array's for became faster only on really big amount of elements. But for list we can see very interesting result.
For looping is faster than other variants as expected. But it was really surprise for me that instance .ForEach is much faster than usual foreach loop!

Ok. But why?

Let's look at IL code of foreach loop in ListForEach method.

IL_0009:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<class ConsoleApplication1.User>::GetEnumerator()  
  IL_000e:  stloc.3
  .try
  {
    IL_000f:  br.s       IL_0027
    IL_0011:  ldloca.s   CS$5$0000
    IL_0013:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ConsoleApplication1.User>::get_Current()
    IL_0018:  stloc.2
    IL_0019:  ldloc.0
    IL_001a:  ldloc.2
    IL_001b:  callvirt   instance class [mscorlib]System.Tuple`2<int32,string> ConsoleApplication1.User::get_Info()
    IL_0020:  callvirt   instance !0 class [mscorlib]System.Tuple`2<int32,string>::get_Item1()
    IL_0025:  add
    IL_0026:  stloc.0
    IL_0027:  ldloca.s   CS$5$0000
    IL_0029:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ConsoleApplication1.User>::MoveNext()
    IL_002e:  brtrue.s   IL_0011
    IL_0030:  leave.s    IL_0040
  }  // end .try
  finally
  {
    IL_0032:  ldloca.s   CS$5$0000
    IL_0034:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ConsoleApplication1.User>
    IL_003a:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_003f:  endfinally
  }  // end handler

As we can see on each iteration we have 2 method calls: Enumerator<class ConsoleApplication1.User>::get_Current() and Enumerator<class ConsoleApplication1.User>::MoveNext()

Now lets look how list's ForEach work:

.method public hidebysig instance void  ForEach(class System.Action`1<!T> action) cil managed
{
  .custom instance void __DynamicallyInvokableAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       91 (0x5b)
  .maxstack  3
  .locals init (int32 V_0,
           int32 V_1)
  IL_0000:  ldarg.1
  IL_0001:  brtrue.s   IL_0009
  IL_0003:  ldc.i4.8
  IL_0004:  call       void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument)
  IL_0009:  ldarg.0
  IL_000a:  ldfld      int32 class System.Collections.Generic.List`1<!T>::_version
  IL_000f:  stloc.0
  IL_0010:  ldc.i4.0
  IL_0011:  stloc.1
  IL_0012:  br.s       IL_003a
  IL_0014:  ldloc.0
  IL_0015:  ldarg.0
  IL_0016:  ldfld      int32 class System.Collections.Generic.List`1<!T>::_version
  IL_001b:  beq.s      IL_0024
  IL_001d:  call       bool System.Runtime.Versioning.BinaryCompatibility::get_TargetsAtLeast_Desktop_V4_5()
  IL_0022:  brtrue.s   IL_0043
  IL_0024:  ldarg.1
  IL_0025:  ldarg.0
  IL_0026:  ldfld      !0[] class System.Collections.Generic.List`1<!T>::_items
  IL_002b:  ldloc.1
  IL_002c:  ldelem     !T
  IL_0031:  callvirt   instance void class System.Action`1<!T>::Invoke(!0)
  IL_0036:  ldloc.1
  IL_0037:  ldc.i4.1
  IL_0038:  add
  IL_0039:  stloc.1
  IL_003a:  ldloc.1
  IL_003b:  ldarg.0
  IL_003c:  ldfld      int32 class System.Collections.Generic.List`1<!T>::_size
  IL_0041:  blt.s      IL_0014
  IL_0043:  ldloc.0
  IL_0044:  ldarg.0
  IL_0045:  ldfld      int32 class System.Collections.Generic.List`1<!T>::_version
  IL_004a:  beq.s      IL_005a
  IL_004c:  call       bool System.Runtime.Versioning.BinaryCompatibility::get_TargetsAtLeast_Desktop_V4_5()
  IL_0051:  brfalse.s  IL_005a
  IL_0053:  ldc.i4.s   32
  IL_0055:  call       void System.ThrowHelper::ThrowInvalidOperationException(valuetype System.ExceptionResource)
  IL_005a:  ret
} // end of method List`1::ForEach

Also decompiled code of ForEach (used Jetbrains DotPeek 2016.2.2):

  public void ForEach(Action<T> action) {
            if( action == null) {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
            }
            Contract.EndContractBlock();

            int version = _version;

            for(int i = 0 ; i < _size; i++) {
                if (version != _version && BinaryCompatibility.TargetsAtLeast_Desktop_V4_5) {
                    break;
                }
                action(_items[i]);
            }

            if (version != _version && BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
                ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
        }

As we can see, there is only one method call: instance void class System.Action1<!T>::Invoke(!0) or action(_items[i]); from decompiled code. It is just regular for loop with one call of our lambda method.

P.S

Micro-optimization is really good way to understand how your code and platform work. Performance is important, and you should always test your apps and looking for the ways to improve it. But keep in mind that in most cases code readability and maintainability is more important then 50ms speed improvements. Don’t sacrifice application maintainability for performance when you don’t actually need it. And if you see that your colleague doesn't use LINQ or foreach because they are a little bit slower, and writes a big piece of code instead of them, please stop them.