Home > Mobile >  Why one delegate is faster than the other?
Why one delegate is faster than the other?

Time:04-19

I am trying to understand the reason for the difference in performance between two delegates. It occurred while I was trying to solve this question. @Enigmativity proposed an alternative way to type-cast, that resulted in a delegate with faster invocation. Here is a minimal version of that code:

delegate void MyAction<T>(T val);
static int Counter;

// My suggestion
static MyAction<T> GetAction1<T>()
    => new MyAction<T>((Action<T>)(object)ActionInt);

// Enigmativity's suggestion
static MyAction<T> GetAction2<T>()
    => (MyAction<T>)(Delegate)(MyAction<int>)ActionInt;

static void ActionInt(int val) { Counter  ; }

There is a custom generic delegate-type MyAction<T>, that has identical signature with the built-in Action<T>. We want to instantiate this delegate from a generic <T> method, and we want to cast it internally to a type-specific ActionInt method. You can see my approach and Enigmativity's approach. It seems that in both cases the type casting occurs during the instantiation of the MyAction<T> delegate. Invoking the resulting delegates should not incur type-casting overhead. At least this is my theory. But when I am measuring the performance of the resulting delegates, Enigmativity's delegate is consistently around 20% faster than mine:

static void Test(string title, MyAction<int> action)
{
    Counter = 0;
    var stopwatch = Stopwatch.StartNew();
    for (int i = 0; i < 100_000_000; i  ) action(i);
    stopwatch.Stop();
    Console.WriteLine($"{title}, Counter: {Counter:#,0}, Duration: {stopwatch.ElapsedMilliseconds:#,0} msec");
}

Test("GetAction1", GetAction1<int>());
Test("GetAction2", GetAction2<int>());
Test("GetAction1", GetAction1<int>());
Test("GetAction2", GetAction2<int>());

Output:

GetAction1, Counter: 100,000,000, Duration: 444 msec
GetAction2, Counter: 100,000,000, Duration: 374 msec
GetAction1, Counter: 100,000,000, Duration: 447 msec
GetAction2, Counter: 100,000,000, Duration: 371 msec

Try it on Fiddle.

Can anyone explain why is this happening?

CodePudding user response:

Using a decompiler, we can discover the following:

Implementation of GetAction1<T>():

IL_0000: ldnull
IL_0001: ldftn        void ConsoleApp1.UnderTest::ActionInt(int32)
IL_0007: newobj       instance void class [System.Runtime]System.Action`1<int32>::.ctor(object, native int)
IL_000c: castclass    class [System.Runtime]System.Action`1<!!0/*T*/>
IL_0011: ldftn        instance void class [System.Runtime]System.Action`1<!!0/*T*/>::Invoke(!0/*T*/)
IL_0017: newobj       instance void class ConsoleApp1.UnderTest/MyAction`1<!!0/*T*/>::.ctor(object, native int)
IL_001c: ret

Implementation of GetAction2<T>():

IL_0000: ldnull
IL_0001: ldftn        void ConsoleApp1.UnderTest::ActionInt(int32)
IL_0007: newobj       instance void class ConsoleApp1.UnderTest/MyAction`1<int32>::.ctor(object, native int)
IL_000c: castclass    class ConsoleApp1.UnderTest/MyAction`1<!!0/*T*/>
IL_0011: ret

You can see in the first case that it is actually creating two delegates, and chaining one to the other.

In the second case it is only creating one delegate.

I can't explain the exact reason for this, but I would think that it's because of the extra cast to object in GetAction1.

  • Related