Search code examples
c#benchmarkingexpression-trees

Calling Delegate.DynamicInvoke vs Func<object>()


I've been benchmarking some code that creates an instance of a type and this result seemed weird to me:

Delegate deleg = Expression.Lambda(Expression.New(_type)).Compile();
// deleg.DynamicInvoke();

vs

Func<object> func = Expression.Lambda<Func<object>>(Expression.New(_type)).Compile();
// func();

Using BenchmarDotNet gives (Mean, Core):

  • Delegate: 501.790 ns
  • Func: 4.710 ns

Anyone knows why the difference is so huge?

Complete benchmarks:

[ClrJob(baseline: true), CoreJob, CoreRtJob]
[RPlotExporter, RankColumn]
public class Benchmarks
{

    private Type _type;
    private ConstructorInfo _constructor;
    private Delegate _delegate;
    private Func<object> _func;

    [GlobalSetup]
    public void GlobalSetup()
    {
        _type = typeof(TestClass);
        _constructor = _type.GetConstructor(Type.EmptyTypes);
        _delegate = Expression.Lambda(Expression.New(_type)).Compile();
        _func = Expression.Lambda<Func<object>>(Expression.New(_type)).Compile();
    }

    [Benchmark(Baseline = true)]
    public object Instanciate_Using_New()
    {
        return new TestClass();
    }

    [Benchmark]
    public object Instanciate_Using_Activator()
    {
        return Activator.CreateInstance(_type);
    }

    [Benchmark]
    public object Instanciate_Using_Constructor()
    {
        return _constructor.Invoke(null);
    }

    [Benchmark]
    public object Instanciate_Using_Expression_Delegate()
    {
        return _delegate.DynamicInvoke();
    }

    [Benchmark]
    public object Instanciate_Using_Expression_Func()
    {
        return _func();
    }

}

Solution

  • The performance difference is caused by the different performance of Invoke() (fast) and DynamicInvoke() (slow). When taking a look at the generated IL of a direct call to a Func<object> typed delegate you can see that the resulting IL will actually call the Invoke() method:

        static void TestInvoke(Func<object> func) {
            func();
        }
    

    The above compiles to IL code looking something like this (in a debug build):

    .method private hidebysig static void TestInvoke(class [mscorlib]System.Func`1<object> func) cil managed {
    .maxstack 8
    
    IL_0000: nop
    
    IL_0001: ldarg.0      // func
    IL_0002: callvirt     instance !0/*object*/ class [mscorlib]System.Func`1<object>::Invoke()
    IL_0007: pop
    
    IL_0008: ret
    
    } // end of method Program::TestInvoke
    

    And the Invoke() method is much faster than the DynamicInvoke() method, since it basically doesn't need to resolve the type of the delegate (as it is already known). The following answer to another question explains the difference of Invoke() and DynamicInvoke() in a bit more detail: https://stackoverflow.com/a/12858434/6122062

    The following very simplified and probably not very accurate test shows a huge difference in perfomance. As you can see there I'm even using the same delegate, just calling it in different ways:

    class Program {
        static void Main(string[] args) {
            var ex = Expression.Lambda<Func<object>>(Expression.New(typeof(object))).Compile();
    
            Stopwatch timer = Stopwatch.StartNew();
            for (int i = 0; i < 1000000; i++) TestInvoke(ex);
            Console.WriteLine($"Invoke():\t\t{timer.Elapsed.ToString()}");
    
            timer = Stopwatch.StartNew();
            for (int i = 0; i < 1000000; i++) TestDynamicInvoke(ex);
            Console.WriteLine($"DynamicInvoke():\t{timer.Elapsed.ToString()}");
    
            Console.ReadKey(true);
        }
    
        static void TestInvoke(Func<object> func) {
            func();
        }
    
        static void TestDynamicInvoke(Delegate deleg) {
            deleg.DynamicInvoke();
        }
    }
    

    Results on my PC at home using a release build, without an attached debugger (as mentioned above I know this simple test might not be very accuarate, but it demonstates the huge difference in performance)

    Invoke():               00:00:00.0080935
    DynamicInvoke():        00:00:00.8382236