Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected poor performance of delegates in C#

I posted this question earlier about dynamically compiling code in C#, and the answer has lead to another question.

One suggestion is that I use delegates, which I tried and they work well. However, they are benching at about 8.4 X slower than direct calls, which makes no sense.

What is wrong with this code?

My results, .Net 4.0, 64 bit, ran exe directly: 62, 514, 530

public static int Execute(int i) { return i * 2; }

private void button30_Click(object sender, EventArgs e)
{
    CSharpCodeProvider foo = new CSharpCodeProvider();

    var res = foo.CompileAssemblyFromSource(
        new System.CodeDom.Compiler.CompilerParameters()
        {
            GenerateInMemory = true,
            CompilerOptions = @"/optimize",                    
        },
        @"public class FooClass { public static int Execute(int i) { return i * 2; }}"
    );

    var type = res.CompiledAssembly.GetType("FooClass");
    var obj = Activator.CreateInstance(type);
    var method = type.GetMethod("Execute");
    int i = 0, t1 = Environment.TickCount, t2;
    //var input = new object[] { 2 };

    //for (int j = 0; j < 10000000; j++)
    //{
    //    input[0] = j;
    //    var output = method.Invoke(obj, input);
    //    i = (int)output;
    //}

    //t2 = Environment.TickCount;

    //MessageBox.Show((t2 - t1).ToString() + Environment.NewLine + i.ToString());

    t1 = Environment.TickCount;

    for (int j = 0; j < 100000000; j++)
    {
        i = Execute(j);
    }

    t2 = Environment.TickCount;

    MessageBox.Show("Native: " + (t2 - t1).ToString() + Environment.NewLine + i.ToString());

    var func = (Func<int, int>) Delegate.CreateDelegate(typeof (Func<int, int>), method);

    t1 = Environment.TickCount;

    for (int j = 0; j < 100000000; j++)
    {
        i = func(j);
    }

    t2 = Environment.TickCount;

    MessageBox.Show("Dynamic delegate: " + (t2 - t1).ToString() + Environment.NewLine + i.ToString());

    Func<int, int> funcL = Execute;

    t1 = Environment.TickCount;

    for (int j = 0; j < 100000000; j++)
    {
        i = funcL(j);
    }

    t2 = Environment.TickCount;

    MessageBox.Show("Delegate: " + (t2 - t1).ToString() + Environment.NewLine + i.ToString());
}
like image 718
IamIC Avatar asked Oct 31 '25 20:10

IamIC


1 Answers

As Hans mentions in the comments on your question, the Execute method is so simple that it's almost certainly being inlined by the jitter in your "native" test.

So what you're seeing isn't a comparison between a standard method call and a delegate invocation, but a comparison between an inlined i * 2 operation and a delegate invocation. (And that i * 2 operation probably boils down to just a single machine instruction, about as fast as you can get.)

Make your Execute methods a bit more complicated to prevent inlining (and/or do it with the MethodImplOptions.NoInlining compiler hint); then you'll get a more realistic comparison between standard method calls and delegate invocations. Chances are that the difference will be negligible in most situations:

[MethodImpl(MethodImplOptions.NoInlining)]
static int Execute(int i) { return ((i / 63.53) == 34.23) ? -1 : (i * 2); }
public static volatile int Result;

private static void Main(string[] args)
{
    const int iterations = 100000000;

    {
        Result = Execute(42);  // pre-jit
        var s = Stopwatch.StartNew();

        for (int i = 0; i < iterations; i++)
        {
            Result = Execute(i);
        }
        s.Stop();
        Console.WriteLine("Native: " + s.ElapsedMilliseconds);
    }

    {
        Func<int, int> func;
        using (var cscp = new CSharpCodeProvider())
        {
            var cp = new CompilerParameters { GenerateInMemory = true, CompilerOptions = @"/optimize" };
            string src = @"public static class Foo { public static int Execute(int i) { return ((i / 63.53) == 34.23) ? -1 : (i * 2); } }";

            var cr = cscp.CompileAssemblyFromSource(cp, src);
            var mi = cr.CompiledAssembly.GetType("Foo").GetMethod("Execute");
            func = (Func<int, int>)Delegate.CreateDelegate(typeof(Func<int, int>), mi);
        }

        Result = func(42);  // pre-jit
        var s = Stopwatch.StartNew();

        for (int i = 0; i < iterations; i++)
        {
            Result = func(i);
        }
        s.Stop();
        Console.WriteLine("Dynamic delegate: " + s.ElapsedMilliseconds);
    }

    {
        Func<int, int> func = Execute;
        Result = func(42);  // pre-jit

        var s = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            Result = func(i);
        }
        s.Stop();
        Console.WriteLine("Delegate: " + s.ElapsedMilliseconds);
    }
}
like image 139
LukeH Avatar answered Nov 02 '25 10:11

LukeH