dotnet|dotnet 委托的实现解析
缘起
最近被问到什么是.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 |
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 |
解析 先上实例代码如下:
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里相关的代码打断点就能进入断点。
当然文中的其他问题也希望有缘人不吝指出。感谢。
推荐阅读
- 链表和数组的区别
- Spring Bean如何初始化的
- Linux|个人学习linux的心得(其实熟悉命令就是记住秘籍)
- 腾讯一面(说一说 MySQL 中索引的底层原理)
- js dy3 感觉需要注意的地方(包含循环)
- js dy2 感觉需要注意的地方(包括数据类型和逻辑分支)
- js dy1 感觉需要注意的地方
- RocketMQ|RocketMQ -- 文件不一致的解决方案
- 走进Chrome拓展开发,定制自己的图床扩展
- python|10 款最好的 Python IDE(十)