Hello,前段时间在园子发了一篇《(译)一个通用快速的反射方法调用》的译文,关于反射“时间性能”比较。这篇文章是对“时间性能”的补充及加入“空间性能”的延伸。
一、时间性能 (InvokeCompare.cs)
n 回顾
在前一篇文章中已经对三种方法调用方式进行过比较。分别为:
1. 直接调用方法
2. 反射并缓存MethodInfo对象进行调用(缓存 + 反射)
3. DynamicMethod动态生成IL方法进行调用
分别进行1000000次循环调用结果如下:
这个结果已经告诉我们反射调用成员的方式是比较慢的,反射为何效率低下,原因是:
a) 搜索:使用 System.Reflection 命名空间中的类型扫描程序集的元数据时,反射要不断的执行字符串的搜索。通常,搜索时不区分大小写的比较,这会更进一步影响性能。
b) 调用:使用反射调用一个成员时。比如调用方法,首先必须将实参打包(pack)成一个数组;在内部,反射必须将这些实参解包(unpack)到线程栈上。此外,在调用方法前,CLR必须检查实参具有正确的数据类型。最后,CLR必须确保调用者有正确的安全权限来访问被调用的成员。
n 补充
今天这边再补充几种成员调用方式进行比较:
1. 反射获取MethodInfo后,挂接到委托上进行调用(缓存 + 反射 + 委托)
——注意构造器和字段不能挂接委托
2. 使用 Activator.CreateInstance() 动态创建对象,在对象上进行方法调用
a) 使用Type.InvokeMember()方式,一种蛋疼的方式
b) 使用Dynamic方式承接对象进行方法调用。(但在分布式架构中我们无法得知方法名,所以我们可能会创建一个“通用方法”----“通用方法”见“空间性能”说明)
结果肯定让你惊喜的发现:当反射成员挂接到委托上,效率能提升到和直接调用“不相上下”。(这个“不相上下”根据类中成员数量会有不同,示例代码中使用了T4模板机制,有兴趣的园友可以自行改变生成规则)
最让你蛋疼的肯定是 Activator.CreateInstance() + InvokeMember() 的方式了(可查看《反射:(9)程序集的加载和反射》),明白了就不要去用了。
n 结论:“时间”性能上比较
(直接调用)>=(缓存 + 反射 +委托)>(DynamicMethod + 委托)>(Activator方式)>(缓存 + 委托)
(在示例代码中我还进行了对单个对象上得多个方法进行调用比较,性能结果顺序是一致的。原本想测试下 Activator.CreateInstance() 对象后,再操作这个对象上多个方法效率会不会高于别的方式)
二、空间性能 (InvokeCompare.cs)
正如大家所知:多数项目都是有“正反”两面的,只有极少数如《人民日报》、《环球日报》、《新闻联播》等项目中才只有“正面”,因为这当中不包括微软,所以下面我再从“空间”上分析下上面几种调用方式。
本示例通过 GC.GetTotalMemory(true) 获取所耗内存字节数,我们还需要了解下“字节换算”:8bit(位)=1Byte(字节) ; 1024Byte(字节)=1KB ; 1024KB=1MB ; 1024MB=1GB ; 1024GB=1TB
1. (缓存 + 反射)方式,缓存MethodInfo
2. (缓存 + 反射 + 委托)方式:
a) 缓存 MethodInfo + 委托集合
b) MethodInfo + 单个委托
3. (DynamicMethod + 委托)方式,缓存委托
4. (Activator.CreateInstance)方式,缓存实例对象
n 结论:“空间”性能上比较
1. (缓存 + 反射)方式:尽管所耗内存是最小的,但因为其“时间性能”所以我们是要避免使用此方式的。我这边列出来主要是想和第二种方式缓存“单个委托”所耗内存进行个参照。
2. (缓存 + 反射 + 委托)方式
a) 委托要及时释放:通过是否缓存委托的内存比较只是想说明委托会占用比较大的内存,因为delegate关键字申明的委托会被编译成继承自Delegate类的类,从上面红色框框标出的缓存“委托集合”和“单个委托”内存差8倍之大,可以说明Delegate类的构造函数有初始化比较多的东西。
b) 缓存“单个委托”方式最佳:从整个结果来看,只缓存“单个委托”是最佳后期绑定方式。(比如事件签名就相当统一)
3. DynamicMethod + 委托,适用于插件机制:就数据来看此方式耗费内存最大,当然用这种方式去比较对它是不公平的,因为正确的适用场合是 “动态构造程序集 + 动态构造方法” 从而实现插件机制,要知道程序集加载到应用程序后是要等到程序关闭才会释放,而这种动态方式的插件机制则可以做到用完即可释放的效果。(实际上我们也可以创建新域Appdomain,让程序集绑定到新域上从而实现相同卸载效果——对于他们之间的优劣我了解甚少,有相关资料的园友还请多多推荐资料)
或则用在委托不好做的地方:比如《Fast Dynamic Property/Field Accessors》
4. Activator.CreateInstance() 方式:很多朋友看到上图后马上得出结论:只需要“缓存 + 反射 + 单个委托”方式。但是我想表达的是“尺有所短,寸有所长”。
a) 服务器:为了节约服务器资源,所以各个模块是根据请求动态加载,并缓存各个功能类,因为多用户连接需求所以各个功能类以“对象池”的方式缓存在服务器。(试想一下缓存MemberInfo,那数量是多么庞大)
b) 客服端:客户端调用服务器的方法时通常要传“类名”和“方法名”两个参数,服务器根据请求 Activator.CreateInstance() 对象,在这个“类”中我们可以声明一个“通用方法”(通用方法的作用是避免使用Type.InvokeMember()去调用成员)根据“方法名”参数去switch调用方法。
从这个例子中可以看到方式(4)是在分布式架构中常用的一种方式,而方式(2)更始用于某一功能块中对MemberInfo进行缓存调用。
三、使用绑定句柄来减少进程的内存耗用 (HandleMemory.cs)
详见:CLR via C#(第三版)
许多应用程序绑定了一组类型(Type 对象)或者类型成员(从MemberInfo 派生的对象),并将这些对象保存在某种形式的一个集合中,以后应用程序搜索这个集合,查找特定的对象,然后调用这个对象。这是一个很好的机制,只是有一个问题:Type和MemberInfo派生对象需要大量内存。因此,如果一个应用程序容纳了太多这样的对象,但只是偶尔调用一下它们,应用程序的内存耗用就急剧增长,对应用程序的性能产生负面影响。
在内部,CLR用一种更精简的方式表示这种信息。CLR之所以为应用程序创建这些对象,只是为了简化开发人员的工作。CLR在运行时本身并不需要这些大对象。如果需要保存缓存大量Type和MemberInfo派生对象,开发人员可以使用运行时句柄(runtime handles)来代替对象,从而减小工作集(占用的内存)。FCL定义了三个运行时句柄类型:RuntimeTypeHandle、RuntimeFieldHandle、RuntimeMethodHandle。三个类型都是值类型,它们只包含一个句柄字段IntPrt,它引用了AppDomain的Loader堆中的一个类型、字段或方法。
1. 转换:RuntimeTypeHandle 与Type
Type.GetTypeFromHandle() 方法
Type 实例的 TypeHandle 属性
2. 转换:RuntimeFieldHandle与FieldInfo
FieldInfo.GetFieldFromHandle() 方法
FieldInfo 实例的 FieldHandle 属性
3. 转换:RuntimeMethodHandle与MethodInfo
MethodInfo.GetMethodFromHandle() 方法
MethodInfo 实例的 MethodHandle 属性
示例运行截图
n 结论:缓存轻量级的运行时句柄 (RuntimeTypeHandle、RuntimeFieldHandle、RuntimeMethodHandle)
推荐阅读:
相关参考:
书籍《CLR via C#(第三版)》