[优化]利用Emit减少反射的性能损失

我很喜欢在程序里使用反射,让事 情变得更简单、灵活。但是在获得便利的同时带来的是性能上的损失。粗略的测试一下,通过反射根据成员名字去访问成员比直接访问慢250倍左右。听起来挺吓 人,但是以时间来算,用我的p4 2.66G的机用反射访问成员一次耗时仅3微秒,对与整个程序的运行周期看来,这占的时间微不足道。
不过我用反射的程度确实过分了点。我很久以前就做过这么一样东西,利用反射实现asp.net控件和数据之间的双向绑定。意料之中的是,有人对我仅为了赋值,取值就使用反射提出质疑。.net阵营不像java那样财大气粗,很多人对性能还是很看重的,風語·深蓝甚至还被上司禁止使用工厂模式,说Activator.CreateInstance会影响性能…
于 是,我开始在思考如何减少反射带来的性能损失。首先元数据可以缓存起来,这样就不必重复执行GetTypes(), GetMembers()等方法了,不过这些操作耗时很少,在程序中用到的地方也少,最重要还是要解决如何能快速的通过传入一个对象和对象的属性名去访问 属性。要大幅提高性能,唯一办法就是不要用反射。。。
还能怎样呢?我想到了一个有这样结构的类:
这个类有GetValue和SetValue方法,通过一堆case去根据参数去读或写相应的属性,用这种方法就能达到我想要的效果,而又不需要反射了。
但是这样针对每一个类,就要有另一个对应的类去处理这样的事情,所以这些负责Get/Set的类应该在运行时动态生成,这就得靠反射最高级的特性-Emit了。
利用System.Reflection.Emit中的类,可以运行时构造类型,并且通过IL去构造成员。当然,生成C#代码用CodeDom编译也可以,但是速度肯定大打折扣了。
跟着我开始动手,先定义IValueHandler接口:

using System;
namespace NoReflection
{
    
internal interface IValueHandler {
        
object GetValue(object o, string expression);
        
void SetValue(object o,string expression, object value);
        
object CreateInstance();
    }
}

然后定义一个叫ObjectUtil类,会用动态构造出用来处理特定类型的IValueHandler的实现类,然后把它放到缓存里。通过几个方法的包装,根据需要调用ValueHandler的GetValue/SetValue。
不 要把构造IL想的太恐怖,其实只要对CLI和MSIL有一定的了解,并且能看懂就行了,因为我可以先用熟悉的C#写好代码,编译后再反汇编去看,大部分可 以抄。当然这个过程还是花了我很多时间,因为IL里头装箱/拆箱得自己管,不同类型的数据转换的代码又各不相同,然后还发现用short form指令的话如果目标地址的偏移量超过了8位/*双鱼座纠正说是7位…*/就会出错,所以xx.s的指令就不能照抄,统统用xx代替。。。
SQLConnection的处理来举例,动态生成的类是这个样子的:

public class ValueHandler : IValueHandler
{
    
// Methods
    public ValueHandler()
    {
    }
    
public override object CreateInstance()
    {
        
return new SQLConnection();
    }
    
public override object GetValue(object obj, string property)
    {
        
switch (property)
        {
            
case "ConnectionString":
                {
                    
return ((SQLConnection) obj).ConnectionString;
                }
            
case "ConnectionTimeout":
                {
                    
return ((SqlConnection) obj).ConnectionTimeout;
                }
            
case "Database":
                {
                    
return ((SqlConnection) obj).Database;
                }
            
case "DataSource":
                {
                    
return ((SqlConnection) obj).DataSource;
                }
            
case "PacketSize":
                {
                    
return ((SqlConnection) obj).PacketSize;
                }
            
case "WorkstationId":
                {
                    
return ((SqlConnection) obj).WorkstationId;
                }
            
case "ServerVersion":
                {
                    
return ((SqlConnection) obj).ServerVersion;
                }
            
case "State":
                {
                    
return ((SqlConnection) obj).State;
                }
            
case "Site":
                {
                    
return ((SqlConnection) obj).Site;
                }
            
case "Container":
                {
                    
return ((SqlConnection) obj).Container;
                }
        }
        
throw new Exception("The property named " + property + " does not exists");
    }
    
public override void SetValue(object obj, string property, object value)
    {
        
switch (property)
        {
            
case "ConnectionString":
                {
                    ((SqlConnection) obj).ConnectionString 
= (string) value;
                    
return;
                }
            
case "Site":
                {
                    ((SqlConnection) obj).Site 
= (ISite) value;
                    
return;
                }
        }
        
throw new Exception("The property named " + property + " does not exists");
    }
}

 ObjectUtil与ASP.NET里的DataBinder用法十分相似,不过DataBinder只能Get,ObjectUtil可以 Get/Set。为了最大的提高性能,我把简单的属性表达式和跨对象的复杂表达式区分对待,例如SqlCommand的CommandText应该使用 ObjectUtil.GetValue(cmd, "CommandText"); 而要得到他的Connection的ConnectionString,则要调用ObjectUtil.GetComplexValue(cmd, "Connection.ConnectionString")。因为GetComplexValue方法多了个对参数Split('.')的操作,如果 是跨对象表达式的则要从左到右一步步获得对象引用,再Get/Set最终对象属性的值。不要小看一次Split(),它已经会使整个过程多花近50%的时 间了。另外也支持对索引器的访问,这里我把表达式规则小改了一下,DataBinder使用和开发语言语法一致的表达式,并且同时兼容VB.NET和C# 语法,例如可以使用DataBinder.Eval(myList, "[0]")或DataBinder.Eval(myList, "(0)")访问索引为0的元素,而ObjectUtil规定了整型的索引直接使用数字,例如OjbectUtil.GetValue(myList, "0"),字符型的索引直接传入字符,如OjbectUtil.GetValue(myList, "someKey")。这样做的原因,一是节省了分析方括号的时间,二是我也省了不少事 🙂
这是生成的处理NameValueCollection的类,比较有代表性,索引器可以是int或string:

public override object GetValue(object obj3, string text1)
{
      
switch (text1)
      {
            
case "AllKeys":
            {
                  
return ((NameValueCollection) obj3).AllKeys;
            }
            
case "Count":
            {
                  
return ((NameValueCollection) obj3).Count;
            }
            
case "Keys":
            {
                  
return ((NameValueCollection) obj3).Keys;
            }
      }
      
if (char.IsDigit(text1[0]))
      {
            
return ((NameValueCollection) obj3)[Convert.ToInt32(text1)];
      }
      
return ((NameValueCollection) obj3)[text1];
}
 

跟着就是测试性能了。我拿ObjectUtil和反射、DataBinder、Spring.Net的ObjectNavigator来做对比。对比结果是:
如果是简单表达式,速度是反射的3.5倍,DataBinder的7倍,ObjectNavigator的15倍。。。这东西真是有java特色啊
复杂表达式,是反射的1.5倍,DataBinder的3.5倍,ObjectNavigator的6.5倍。
由于ObjectUtil用法和DataBinder和ObjectNavigator类似,比直接反射简单的多,以他们为参照比较合适。成绩算不错了 🙂
CreateInstance是后来顺便加上的,用默认的构造函数去创建对象。看来还很有必要,有些类型的对象用反射创建耗时极长,我也不知原因,就拿SqlCommand来说,用ObjectUtil创建比用反射快超过40倍
当然,在实际应用中,它带来的效果通常不太明显,原因就是我前面所说的,在整个程序执行期间,反射耗时仅占极少一部分。例如我曾经用NHibernate 0.8做测试,性能仅提高了3%左右,因为大部分时间花在了分析HQL和数据库读取上。但是用在大部分时间在反射的ObjectComparer上,效果不错,用ObjectUtil替换后速度是原来3倍
源码 /Files/Yok/NoReflection.rar
生成的类 /Files/Yok/EmittedDlls.rar

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏