dotnet 委托的实现解析

本文探讨.NET中的委托,对比执行效率,如直接调用属性与通过委托调用的差异,并解析委托的元数据与IL代码。通过示例代码展示委托继承自MulticastDelegate而非Delegate的事实,提出对委托执行机制的猜测,同时分享了在混合调试托管代码时遇到的挑战和疑问。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Python微信订餐小程序课程视频

https://edu.youkuaiyun.com/course/detail/36074

Python实战量化交易理财系统

https://edu.youkuaiyun.com/course/detail/35475

缘起

最近被问到什么是.Net中的委托。问题虽然简单却无从回答。只能说委托是托管世界的函数指针,这么说没啥大毛病,但也都是毛病(当时自己也知道这么说不太对,不过自己不太爱用这个也没准备确实没有更好的答案)。

执行效率

正巧前段时间看Core CLR的文档看到不同方式调用函数效率的比较正巧有这个,摘录如下。这段内容在 clr官方文档
为什么反射很慢 ?里。

Reading a Property (‘Get’)

MethodMeanStdErrScaledBytes Allocated/Op
GetViaProperty0.2159 ns0.0047 ns1.000.00
GetViaDelegate1.8903 ns0.0082 ns8.820.00
GetViaILEmit2.9236 ns0.0067 ns13.640.00
GetViaCompiledExpressionTrees12.3623 ns0.0200 ns57.650.00
GetViaFastMember35.9199 ns0.0528 ns167.520.00
GetViaReflectionWithCaching125.3878 ns0.2017 ns584.780.00
GetViaReflection197.9258 ns0.2704 ns923.080.01
GetViaDelegateDynamicInvoke842.9131 ns1.2649 ns3,931.17419.04

Writing a Property (‘Set’)

MethodMeanStdErrScaledBytes Allocated/Op
SetViaProperty1.4043 ns0.0200 ns6.550.00
SetViaDelegate2.8215 ns0.0078 ns13.160.00
SetViaILEmit2.8226 ns0.0061 ns13.160.00
SetViaCompiledExpressionTrees10.7329 ns0.0221 ns50.060.00
SetViaFastMember36.6210 ns0.0393 ns170.790.00
SetViaReflectionWithCaching214.4321 ns0.3122 ns1,000.0798.49
SetViaReflection287.1039 ns0.3288 ns1,338.99115.63
SetViaDelegateDynamicInvoke922.4618 ns2.9192 ns4,302.17390.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表里找到了委托对象类型定义,毕竟一切皆对象,这个应该和事件是一个处理方法。

NameBaseTypeFieldListMethodList
SayHi0x100000E0x40000000x600006

剩下的表暂时先不看了(主要时间太长不记得类型方法在表里是咋对应起来的了)

下面先把类型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的官方文档处理了,现在只剩下无奈与黔驴技穷了。
当然文中的其他问题也希望有缘人不吝指出。感谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值