Chuu
Chuu

Reputation: 4509

Why is there such a large difference in the performance of different ways to pass delegates?

I was attempting to compare three different ways of passing a delegate to a function in C# -- by lambda, by delegate, and by direct reference. What really surprised me was the direct reference method (i.e. ComputeStringFunctionViaFunc(object[i].ToString)) was six times slower than the other methods. Does anyone know why this is?

The complete code is as below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.CompilerServices;

namespace FunctionInvocationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            object[] objectArray = new object[10000000];
            for (int i = 0; i < objectArray.Length; ++i) { objectArray[i] = new object(); }

            ComputeStringFunction(objectArray[0]);
            ComputeStringFunctionViaFunc(objectArray[0].ToString);
            ComputeStringFunctionViaFunc(delegate() { return objectArray[0].ToString(); });
            ComputeStringFunctionViaFunc(() => objectArray[0].ToString());

            System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunction(objectArray[i]);
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            s.Reset();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunctionViaFunc(delegate() { return objectArray[i].ToString(); });
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            s.Reset();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunctionViaFunc(objectArray[i].ToString);
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            s.Reset();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunctionViaFunc(() => objectArray[i].ToString());
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            Console.ReadLine();
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void ComputeStringFunction(object stringFunction)
        {
        }

        public static void ComputeStringFunctionViaFunc(Func<string> stringFunction)
        {
        }
    }
}

Upvotes: 10

Views: 416

Answers (2)

Chris Shain
Chris Shain

Reputation: 51329

Lets examine what you are doing in each case:

This guy doesnt "create" a function at all. It looks up an item (in this case an object) in an array, and passes the item as the parameter to a function:

// The cost of doing the array lookup happens right here, before 
// ComputeStringFunction is called
ComputeStringFunction(objectArray[i]);

This one creates a parameterless delegate and passes it to a function. The delegate itself is never called:

// Because ComputeStringFunctionViaFunc doesn't do anything, the
// statement objectArray[i] is never evaluated, so the only cost 
// is that of creating a delegate
ComputeStringFunctionViaFunc(delegate() { return objectArray[i].ToString(); });

This one does the same as the first, except instead of passing the item immedately after retrieving it from the array, it calls .ToString() on it. Again, no function is created here:

Like the first, this one has the cost of the array lookup up front, but then creates a delegate referencing the .ToString method of the item (thanks @hvd for catching that). Like the others, .ToString is never evaluated. The cost is (again, thanks @hvd) that of looking up the virtual method.

// The cost of doing the array lookup happens right here
ComputeStringFunctionViaFunc(objectArray[i].ToString);

Finally, this one creates a function using a lambda and a closure over an array item, and passes that lambda to a function. Depending on the functions signature, the lambda may be compiled or not:

// Again, we create a delegate but don't call it, so the array
// lookup and .ToString are never evaluated.
ComputeStringFunctionViaFunc(() => objectArray[i].ToString());

The important thing to note here is that evaluation of the array lookup is delayed in the second and fourth, while it is not delayed in the first and third.

These tests are somewhat nonsensical because they all do completely different things. There are almost certainly better ways of timing delegate creation.

Upvotes: 4

user743382
user743382

Reputation:

After fixing up your code to actually call ToString() / stringFunction(), and measuring using Mono 2.10.9:

ComputeStringFunctionViaFunc(objectArray[i].ToString); is slow because object.ToString is virtual. Each object is checked in case it overrides ToString and the overridden ToString should be called. Your other delegates are created to refer to a non-virtual function (fast), which directly calls a virtual function (also fast). The fact that this is the cause can be seen when modifying the generated IL to change

ldelem.ref
dup 
ldvirtftn instance string object::ToString()

to

ldelem.ref
ldftn instance string object::ToString()

which always refers to object.ToString, never an overriding function. The three methods then all take about the same time.

Update: one additional method, to bind directly to objectArray[i] but still call ToString virtually:

for (int i = 0; i < objectArray.Length; ++i)
{
    ComputeStringFunctionViaFunc(objectArray[i].ToStringHelper);
}

static class Extensions
{
    public static string ToStringHelper(this object obj)
    {
        return obj.ToString();
    }
}

also gives roughly the same timings as the other non-virtual delegates.

Upvotes: 6

Related Questions