緣起
最近被問到什麼是.Net中的委託。問題雖然簡單卻無從回答。只能說委託是託管世界的函式指標,這麼說沒啥大毛病,但也都是毛病(當時自己也知道這麼說不太對,不過自己不太愛用這個也沒準備確實沒有更好的答案)。
執行效率
正巧前段時間看Core CLR的文件看到不同方式呼叫函式效率的比較正巧有這個,摘錄如下。這段內容在 clr官方文件
的 為什麼反射很慢 ?裡。
Reading a Property (‘Get’)
Method | Mean | StdErr | Scaled | Bytes Allocated/Op |
---|---|---|---|---|
GetViaProperty | 0.2159 ns | 0.0047 ns | 1.00 | 0.00 |
GetViaDelegate | 1.8903 ns | 0.0082 ns | 8.82 | 0.00 |
GetViaILEmit | 2.9236 ns | 0.0067 ns | 13.64 | 0.00 |
GetViaCompiledExpressionTrees | 12.3623 ns | 0.0200 ns | 57.65 | 0.00 |
GetViaFastMember | 35.9199 ns | 0.0528 ns | 167.52 | 0.00 |
GetViaReflectionWithCaching | 125.3878 ns | 0.2017 ns | 584.78 | 0.00 |
GetViaReflection | 197.9258 ns | 0.2704 ns | 923.08 | 0.01 |
GetViaDelegateDynamicInvoke | 842.9131 ns | 1.2649 ns | 3,931.17 | 419.04 |
Writing a Property (‘Set’)
Method | Mean | StdErr | Scaled | Bytes Allocated/Op |
---|---|---|---|---|
SetViaProperty | 1.4043 ns | 0.0200 ns | 6.55 | 0.00 |
SetViaDelegate | 2.8215 ns | 0.0078 ns | 13.16 | 0.00 |
SetViaILEmit | 2.8226 ns | 0.0061 ns | 13.16 | 0.00 |
SetViaCompiledExpressionTrees | 10.7329 ns | 0.0221 ns | 50.06 | 0.00 |
SetViaFastMember | 36.6210 ns | 0.0393 ns | 170.79 | 0.00 |
SetViaReflectionWithCaching | 214.4321 ns | 0.3122 ns | 1,000.07 | 98.49 |
SetViaReflection | 287.1039 ns | 0.3288 ns | 1,338.99 | 115.63 |
SetViaDelegateDynamicInvoke | 922.4618 ns | 2.9192 ns | 4,302.17 | 390.99 |
上表分別列出了讀取和設定屬性通過不同方式的耗時等結果,我們可以看到直接通過屬性讀取和通過委託讀取速度的平均值相差了接近10倍。這麼看委託顯然就不是函式指標了(函式指標的效能損失很小),那麼下面就具體看下究竟是啥。
解析
先上例項程式碼如下:
internal class HelloWorld
{
public static void HelloWorld1()
{
Console.WriteLine("hello world1");
}
public delegate void SayHi();
public void Main()
{
SayHi? helloWorld = new SayHi(HelloWorld1);
helloWorld.Invoke();
}
}
很簡單的程式碼,編譯後用ILSpy開啟。
後設資料與IL
首先看下後設資料表,毫不例外的在02 TypeDef表裡找到了委託物件型別定義,畢竟一切皆物件,這個應該和事件是一個處理方法。
Name | BaseType | FieldList | MethodList |
---|---|---|---|
SayHi | 0x100000E | 0x4000000 | 0x600006 |
剩下的表暫時先不看了(主要時間太長不記得型別方法在表裡是咋對應起來的了)
下面先把型別SayHi的定義相關的IL程式碼貼出來
.class nested public auto ansi sealed SayHi
extends [System.Runtime]System.MulticastDelegate
{
// Methods
.method public hidebysig specialname rtspecialname
instance void .ctor (
object 'object',
native int 'method'
) runtime managed
{
} // end of method SayHi::.ctor
.method public hidebysig newslot virtual
instance void Invoke () runtime managed
{
} // end of method SayHi::Invoke
.method public hidebysig newslot virtual
instance class [System.Runtime]System.IAsyncResult BeginInvoke (
class [System.Runtime]System.AsyncCallback callback,
object 'object'
) runtime managed
{
} // end of method SayHi::BeginInvoke
.method public hidebysig newslot virtual
instance void EndInvoke (
class [System.Runtime]System.IAsyncResult result
) runtime managed
{
} // end of method SayHi::EndInvoke
} // end of class SayHi
這裡第一個意外出來了,我一直以為委託是繼承自System.Delegate
但是沒想到卻是繼承自System.MulticastDelegate
。大家都知道後者繼承前者主要就是是為了實現 += 這種多播委託的方式(也就是天天寫事件用的這種方式)。 那麼委託像事件那麼註冊好多個就是合情又合理了。也就是如下這種。
internal class HelloWorld
{
public static void HelloWorld1()
{
Console.WriteLine("hello world1");
}
public static void HelloWorld2()
{
Console.WriteLine("hello world2");
}
public delegate void SayHi();
public void Main()
{
SayHi? helloWorld = new SayHi(HelloWorld1);
helloWorld += HelloWorld2;
helloWorld.Invoke();
}
}
果然是可以的,可惜大家(我們組的其他同事)寧願用事件的方式,從來沒見這麼用過。
IL裡定義的其他方法也沒啥稀奇的Invoke這類的都是編譯器加進去的,直接呼叫clr裡處理,這裡看不到實現。
小小的結論與一些疑惑
先說結論: (大膽猜測:)委託實際上和事件類似都是編譯成一個物件,然後JIT執行到這個stub時再以FCall的形式(也許是QCall(FQ傻傻分不清),畢竟是動態生成的類不是很瞭解)呼叫到CLR裡。我不愛用這個果然是對的。
再說說疑惑:
實際上最近在混合除錯託管程式碼時遇到了很大問題。也就是
- 只除錯託管程式碼或者System.Private.CoreLib時沒有問題。
- 只除錯core clr時也沒問題(雖然大部分看不懂)。
- 一旦混合除錯時(託管程式碼呼叫clr的功能如 GetHashcode 或者 lock時)就有很多函式進不去,但是也不是也不是完全進不去,還是可以看見一部分混合呼叫的堆疊的。導致我現在很多隻能靠猜,例如GetHashcode()是以FCall的形式呼叫到CLR裡,直接在Core CLR裡相關的程式碼打斷點就能進入斷點。
希望有緣人解答一下,我已經按clr的官方文件處理了,現在只剩下無奈與黔驢技窮了。
當然文中的其他問題也希望有緣人不吝指出。感謝。