我試圖了解兩位代表之間表現差異的原因。它發生在我試圖解決'">這個問題時。@Enigmativity提出了一種替代型別轉換的方法,這導致委托的呼叫速度更快。這是該代碼的最小版本:
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 ; }
有一個自定義的通用委托型別MyAction<T>,它與內置的Action<T>. 我們想從一個泛型<T>方法實體化這個委托,并且我們想在內部將它轉換為一個特定于型別的ActionInt方法。你可以看到我的方法和 Enigmativity 的方法。似乎在這兩種情況下,型別轉換都發生在MyAction<T>委托的實體化期間。呼叫生成的委托不應產生型別轉換開銷。至少這是我的理論。但是當我衡量最終代表的表現時,Enigmativity 的代表始終比我的快 20% 左右:
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>());
輸出:
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
在 Fiddle 上試試。
誰能解釋為什么會這樣?
uj5u.com熱心網友回復:
使用反編譯器,我們可以發現以下內容:
實施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
實施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
您可以在第一種情況下看到它實際上是創建了兩個委托,并將一個委托鏈接到另一個委托。
在第二種情況下,它只創建一個委托。
我無法解釋其確切原因,但我認為這是因為objectin的額外演員陣容GetAction1。
似乎有一個更快的實作,即:
public static MyAction<T> GetAction3<T>()
=> x => ActionInt((int)(object)x);
這會生成更長的 IL 代碼:
IL_0000: ldsfld class ConsoleApp1.UnderTest/MyAction`1<!0/*T*/> class ConsoleApp1.UnderTest/'<>c__4`1'<!!0/*T*/>::'<>9__4_0'
IL_0005: dup
IL_0006: brtrue.s IL_0021
IL_0008: pop
IL_0009: ldsfld class ConsoleApp1.UnderTest/'<>c__4`1'<!0/*T*/> class ConsoleApp1.UnderTest/'<>c__4`1'<!!0/*T*/>::'<>9'
IL_000e: ldftn instance void class ConsoleApp1.UnderTest/'<>c__4`1'<!!0/*T*/>::'<GetAction3>b__4_0'(!0/*T*/)
IL_0014: newobj instance void class ConsoleApp1.UnderTest/MyAction`1<!!0/*T*/>::.ctor(object, native int)
IL_0019: dup
IL_001a: stsfld class ConsoleApp1.UnderTest/MyAction`1<!0/*T*/> class ConsoleApp1.UnderTest/'<>c__4`1'<!!0/*T*/>::'<>9__4_0'
IL_001f: stloc.0 // V_0
IL_0020: ldloc.0 // V_0
IL_0021: ret
然而,呼叫GetAction3()和執行它回傳的操作都更快。
這是我測驗過的基準程式:
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
namespace ConsoleApp1;
public static class Program
{
public static void Main()
{
var summary = BenchmarkRunner.Run<UnderTest>();
}
}
public class UnderTest
{
public delegate void MyAction<T>(T val);
public static int counter;
public static MyAction<T> GetAction1<T>()
=> new MyAction<T>((Action<T>)(object)ActionInt);
// Enigmativity's suggestion
public static MyAction<T> GetAction2<T>()
=> (MyAction<T>)(Delegate)(MyAction<int>)ActionInt;
public static MyAction<T> GetAction3<T>()
=> x => ActionInt((int)(object)x);
public static MyAction<int> Act1 = GetAction1<int>();
public static MyAction<int> Act2 = GetAction2<int>();
public static MyAction<int> Act3 = GetAction3<int>();
static void ActionInt(int val) { counter ; }
[Benchmark]
public void Action1()
{
_ = GetAction1<int>();
}
[Benchmark]
public void Action2()
{
_ = GetAction2<int>();
}
[Benchmark]
public void Action3()
{
_ = GetAction3<int>();
}
[Benchmark]
public void RunAction1()
{
Act1(0);
}
[Benchmark]
public void RunAction2()
{
Act2(0);
}
[Benchmark]
public void RunAction3()
{
Act3(0);
}
}
結果:
| Method | Mean | Error | StdDev |
|----------- |-----------:|----------:|----------:|
| Action1 | 13.3355 ns | 0.1670 ns | 0.1480 ns |
| Action2 | 6.9685 ns | 0.1313 ns | 0.1228 ns |
| Action3 | 1.3437 ns | 0.0321 ns | 0.0285 ns |
| RunAction1 | 2.4100 ns | 0.0454 ns | 0.0425 ns |
| RunAction2 | 1.6493 ns | 0.0594 ns | 0.0527 ns |
| RunAction3 | 0.8347 ns | 0.0295 ns | 0.0276 ns |
當然,沒有一個動作實際上使用int傳遞給它們的引數,因為它們都只是呼叫ActionInt()而忽略了它的引數。
我想您也可以將其實作為:
public static MyAction<T> GetAction3<T>()
=> _ => ActionInt(0);
這可能會更快,但我還沒有嘗試過。
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/459253.html
上一篇:為任何可迭代物件創建異步迭代器
