1

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();
    }

}
Haytam
  • 4,643
  • 2
  • 20
  • 43
  • What if you measure invocation together with compilation? – Johnny Apr 23 '19 at 19:57
  • No, compilations are done in the GlobalSetup; – Haytam Apr 23 '19 at 19:58
  • 1
    You should make sure your questions are clear in their goals. It is only by reading the code that one discovers that your question is not about performance differences in compiling, it is about calling the code. – Lasse V. Karlsen Apr 23 '19 at 22:10
  • @LasseVågsætherKarlsen What? how is the question not about performance? – Haytam Apr 23 '19 at 22:20
  • 1
    It is about performance, but you're profiling invoking the compiled code, not profiling compiling the code. That's what I meant. You say you're benchmarking "some code that creates an instance of a type", and then post one piece of code vs. another, but you're not benchmarking those pieces of code, you're benchmarking using the results. – Lasse V. Karlsen Apr 23 '19 at 22:28
  • Ah, well I changed the title to reflect that. Anyways, it's answered. – Haytam Apr 23 '19 at 22:30

1 Answers1

4

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
bassfader
  • 1,346
  • 1
  • 11
  • 15