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
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
.