C# 之 反射性能优化1

  反射是一种很重要的技术,然而它与直接调用相比性能要慢不少,所以如何优化反射性能也就成为一个不得不面对的问题。 目前最多见的优化反射性能的方法就是采用委托:用委托的方式调用须要反射调用的方法(或者属性、字段)。html

  目前最多见也就是二种方法:Emit, ExpressionTree 。其中ExpressionTree可认为是Emit方法的简化版本, 因此Emit是最根本的方法,它采用在运行时动态构造一段IL代码来包装须要反射调用的代码, 这段动态生成的代码知足某个委托的签名,所以最后能够采用委托的方式代替反射调用。缓存

1、用Emit方法优化反射

  若是咱们须要设计本身的数据访问层,那么就须要动态建立全部的数据实体对象,尤为是还要为每一个数据实体对象的属性赋值, 这里就要涉及用反射的方法对属性执行写操做,为了优化这种反射场景的性能,咱们能够用下面的方法来实现:安全

public delegate void SetValueDelegate(object target, object arg);

public static class DynamicMethodFactory
{
    public static SetValueDelegate CreatePropertySetter(PropertyInfo property)
    {
        if( property == null )
            throw new ArgumentNullException("property");

        if( !property.CanWrite )
            return null;

        MethodInfo setMethod = property.GetSetMethod(true);

        DynamicMethod dm = new DynamicMethod("PropertySetter", null,
            new Type[] { typeof(object), typeof(object) }, property.DeclaringType, true);

        ILGenerator il = dm.GetILGenerator();

        if( !setMethod.IsStatic ) {
            il.Emit(OpCodes.Ldarg_0);
        }
        il.Emit(OpCodes.Ldarg_1);

        EmitCastToReference(il, property.PropertyType);
        if( !setMethod.IsStatic && !property.DeclaringType.IsValueType ) {
            il.EmitCall(OpCodes.Callvirt, setMethod, null);
        }
        else
            il.EmitCall(OpCodes.Call, setMethod, null);

        il.Emit(OpCodes.Ret);

        return (SetValueDelegate)dm.CreateDelegate(typeof(SetValueDelegate));
    }
    
    private static void EmitCastToReference(ILGenerator il, Type type)
    {
        if( type.IsValueType )
            il.Emit(OpCodes.Unbox_Any, type);
        else
            il.Emit(OpCodes.Castclass, type);
    }
}

  如今能够用下面的测试代码检验委托调用带来的性能改进:性能优化

Console.WriteLine(System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion());

int count = 1000000;

OrderInfo testObj = new OrderInfo();
PropertyInfo propInfo = typeof(OrderInfo).GetProperty("OrderID");

Console.Write("直接访问花费时间:       ");
Stopwatch watch1 = Stopwatch.StartNew();

for( int i = 0; i < count; i++ )
    testObj.OrderID = 123;

watch1.Stop();
Console.WriteLine(watch1.Elapsed.ToString());

SetValueDelegate setter2 = DynamicMethodFactory.CreatePropertySetter(propInfo);
Console.Write("EmitSet花费时间:        ");
Stopwatch watch2 = Stopwatch.StartNew();

for( int i = 0; i < count; i++ )
    setter2(testObj, 123);

watch2.Stop();
Console.WriteLine(watch2.Elapsed.ToString());

Console.Write("纯反射花费时间:        ");
Stopwatch watch3 = Stopwatch.StartNew();

for( int i = 0; i < count; i++ )
    propInfo.SetValue(testObj, 123, null);

watch3.Stop();
Console.WriteLine(watch3.Elapsed.ToString());

Console.WriteLine("-------------------");
Console.WriteLine("{0} / {1} = {2}",
    watch3.Elapsed.ToString(),
    watch1.Elapsed.ToString(),
    watch3.Elapsed.TotalMilliseconds / watch1.Elapsed.TotalMilliseconds);

Console.WriteLine("{0} / {1} = {2}", 
    watch3.Elapsed.ToString(),
    watch2.Elapsed.ToString(),
    watch3.Elapsed.TotalMilliseconds / watch2.Elapsed.TotalMilliseconds);

Console.WriteLine("{0} / {1} = {2}",
    watch2.Elapsed.ToString(),
    watch1.Elapsed.ToString(),
    watch2.Elapsed.TotalMilliseconds / watch1.Elapsed.TotalMilliseconds);

我用VS2008 (.net 3.5 , CLR 2.0) 测试能够获得如下结果:
数据结构

  从结果能够看出:
  1. 反射调用所花时间是直接调用的2629倍,
  2. 反射调用所花时间是Emit生成的Set委托代码的82倍,
  3. 运行Emit生成的Set委托代码所花时间是直接调用的31倍。多线程

  虽然Emit比直接调用还有30倍的差距,但仍是比反射调用快80倍左右。并发

  有意思的是,一样的代码,若是用VS2012 ( .net 4.5 , CLR 4.0) 测试能够获得如下结果:
  app

  感谢zhangweiwen 在博客中展现了CRL 4.0对反射的性能改进, 在他的博客中还提供了一种采用表达式树的优化版本,以及包含一个泛型的强类型的版本。性能

2、Delegate.CreateDelegate也能建立委托测试

  若是咱们观察CreatePropertySetter的实现代码,发现这个方法的本质就是建立一个委托:

public static SetValueDelegate CreatePropertySetter(PropertyInfo property)
{
    // ..... 省略前面已贴过的代码
    return (SetValueDelegate)dm.CreateDelegate(typeof(SetValueDelegate));
}

  看到这里,让我想起Delegate.CreateDelegate方法也能建立一个委托,例如:

OrderInfo testObj = new OrderInfo();
PropertyInfo propInfo = typeof(OrderInfo).GetProperty("OrderID");

Action<OrderInfo, int> setter = (Action<OrderInfo, int>)Delegate.CreateDelegate(typeof(Action<OrderInfo, int>), null, propInfo.GetSetMethod());

setter(testObj, 123);

  显然,这是一种很直观的方法,能够获得一个强类型的委托。

  然而,这种方法仅限有一种适用场景:明确知道要访问某个类型的某个属性或者方法,由于咱们要提供类型参数。 例如:我要写个关键字过滤的HttpMoudle,它须要修改HttpRequest.Form对象的IsReadOnly属性,因为IsReadOnly在NameObjectCollectionBase类型中已申明为protected访问级别, 因此我只能反射操做它了,并且还须要很频繁的设置它。

  在绝大部分反射场景中,例如数据访问层中从DataReader或者DataRow加载数据实体, 咱们不可能事先知道要加载哪些类型,更不可能知道要加载哪些数据成员,所以就不可能给泛型委托的类型参数赋值, 这个方法看起来也就行不通了。

  若是您不信的话,能够看下面修改后的代码:

OrderInfo testObj = new OrderInfo();
PropertyInfo propInfo = typeof(OrderInfo).GetProperty("OrderID");

//Action<OrderInfo, int> setter = (Action<OrderInfo, int>)Delegate.CreateDelegate(
//    typeof(Action<OrderInfo, int>), null, propInfo.GetSetMethod());

Action<object, object> setter = (Action<object, object>)Delegate.CreateDelegate(
    typeof(Action<object, object>), null, propInfo.GetSetMethod());

setter(testObj, 123);

Console.WriteLine(testObj.OrderID);

虽然能经过编译,但会在运行时报错:

  在不少时候,咱们只能在运行时获得以Type对象表示的类型,接受object类型才是通用的解决方案。 然而,前面的代码证实了咱们不能简单将委托类型从Action<OrderInfo, int>修改成Action<object, object> 。

  真的没有办法了吗?

  虽然Emit已经是很成熟的优化方案,可我仍是但愿试试 Delegate.CreateDelegate !

  用Delegate.CreateDelegate优化反射

  当咱们用Delegate.CreateDelegate从一个MethodInfo对象建立委托时, 委托的签名必须和MethodInfo表示的方法签名相匹配(有可能不一致), 因此这种方法获得的委托注定是一种强类型的委托。 如今的问题是:咱们在运行时构造与指定MethodInfo匹配的委托,如何将Type对象转换成泛型委托的类型参数?

  为了解决这个问题,我采用了泛型类来解决泛型委托的类型参数问题:

public class SetterWrapper<TTarget, TValue> 
{
    private Action<TTarget, TValue> _setter;

    public SetterWrapper(PropertyInfo propertyInfo)
    {
        if( propertyInfo == null )
            throw new ArgumentNullException("propertyInfo");

        if( propertyInfo.CanWrite == false )
            throw new NotSupportedException("属性不支持写操做。");

        MethodInfo m = propertyInfo.GetSetMethod(true);
        _setter = (Action<TTarget, TValue>)Delegate.CreateDelegate(typeof(Action<TTarget, TValue>), null, m);
    }
    
    public void SetValue(TTarget target, TValue val)
    {
        _setter(target, val);
    }

  我用泛型类把Delegate.CreateDelegate的问题解决了,可是如何建立这个类型的实例呢?
  能够用Type.MakeGenericType()方法来解决:

public static object CreatePropertySetterWrapper(PropertyInfo propertyInfo)
{
    if( propertyInfo == null )
        throw new ArgumentNullException("propertyInfo");
    if( propertyInfo.CanWrite == false )
        throw new NotSupportedException("属性不支持写操做。");

    MethodInfo mi = propertyInfo.GetSetMethod(true);

    if( mi.GetParameters().Length > 1 )
        throw new NotSupportedException("不支持构造索引器属性的委托。");

    Type instanceType = typeof(SetterWrapper<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType);
    return Activator.CreateInstance(instanceType, propertyInfo);
}

  如今问题并无结束,我又如何调用那些泛型类型实例的委托呢?
  这里还有另外一个问题要解决:调用方法须要支持object类型(知足通用性)。
  我想到了定义一个接口来解决:

public interface ISetValue
{
    void Set(object target, object val);
}

  而后让SetterWrapper实现ISetValue接口:

public class SetterWrapper<TTarget, TValue> : ISetValue
{
    // ..... 省略前面已贴过的代码

    void ISetValue.Set(object target, object val)
    {
        _setter((TTarget)target, (TValue)val);
    }
}

  还有前面的CreatePropertySetterWrapper方法也须要再次调整返回值类型:

public static ISetValue CreatePropertySetterWrapper(PropertyInfo propertyInfo)
{
    // ..... 省略前面已贴过的代码
    return (ISetValue)Activator.CreateInstance(instanceType, propertyInfo);
}

  考虑到有些特定场景下须要用反射的方式重复操做某一个属性,使用强类型的方法能够避免拆箱装箱,
  因此我保留了前面的SetValue方法,让它提供更好的性能,知足一些特定场景的须要。
  所以,如今的SetterWrapper类型有二种使用方法,能够提供二种性能不一样的实现方法。

  如今能够增长二段测试代码来测试它的性能了:

Console.Write("泛型委托花费时间:       ");
SetterWrapper<OrderInfo, int> setter3 = new SetterWrapper<OrderInfo, int>(propInfo);
Stopwatch watch4 = Stopwatch.StartNew();

for( int i = 0; i < count; i++ )
    setter3.SetValue(testObj, 123);

watch4.Stop();
Console.WriteLine(watch4.Elapsed.ToString());


Console.Write("通用接口花费时间:       ");
ISetValue setter4 = GetterSetterFactory.CreatePropertySetterWrapper(propInfo);
Stopwatch watch5 = Stopwatch.StartNew();

for( int i = 0; i < count; i++ )
    setter4.Set(testObj, 123);

watch5.Stop();
Console.WriteLine(watch5.Elapsed.ToString());

 

  测试结果以下:

  测试结果代表:强类型的泛型委托的速度比Emit生成的Set委托要快,可是基于通用接口的方法调用因为多了一层包装就比Emit方案要略慢一点。

  完整的属性优化方案

  前面介绍了为属性赋值这类反射案例的优化方案,那么怎么优化读取属性的反射操做呢?

  其实思路差很少:
  1. 在泛型类中调用Delegate.CreateDelegate,获得一个Func<TTarget, TValue>,
  2. 定义一个IGetValue接口,提供一个方法: object Get(object target);
  3. 让泛型类实现IGetValue接口
  4. 提供一个工厂方法实例化泛型类的实例。
  相关代码以下:

public interface IGetValue
{
    object Get(object target);
}

public static class GetterSetterFactory
{
    public static IGetValue CreatePropertyGetterWrapper(PropertyInfo propertyInfo)
    {
        if( propertyInfo == null )
            throw new ArgumentNullException("propertyInfo");
        if( propertyInfo.CanRead == false )
            throw new InvalidOperationException("属性不支持读操做。");

        MethodInfo mi = propertyInfo.GetGetMethod(true);

        if( mi.GetParameters().Length > 0 )
            throw new NotSupportedException("不支持构造索引器属性的委托。");
        
        Type instanceType = typeof(GetterWrapper<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType);
        return (IGetValue)Activator.CreateInstance(instanceType, propertyInfo);
    }
}

public class GetterWrapper<TTarget, TValue> : IGetValue
{
    private Func<TTarget, TValue> _getter;

    public GetterWrapper(PropertyInfo propertyInfo)
    {
        if( propertyInfo == null )
            throw new ArgumentNullException("propertyInfo");

        if( propertyInfo.CanRead == false )
            throw new InvalidOperationException("属性不支持读操做。");

        MethodInfo m = propertyInfo.GetGetMethod(true);
        _getter = (Func<TTarget, TValue>)Delegate.CreateDelegate(typeof(Func<TTarget, TValue>), null, m);
    }
    
    public TValue GetValue(TTarget target)
    {
        return _getter(target);
    }
    object IGetValue.Get(object target)
    {
        return _getter((TTarget)target);
    }
}

  前面的代码优化了实例属性的反射读写性能问题,可是还有极少数时候咱们还须要处理静态属性,那么咱们还须要再定义二个泛型类来解决:

public class StaticGetterWrapper<TValue> : IGetValue
{
    private Func<TValue> _getter;

    // ............
}

public class StaticSetterWrapper<TValue> : ISetValue
{
    private Action<TValue> _setter;

    // ............
}

  前面看到的工厂方法也要调整,完整代码以下:

public static ISetValue CreatePropertySetterWrapper(PropertyInfo propertyInfo)
{
    if( propertyInfo == null )
        throw new ArgumentNullException("propertyInfo");
    if( propertyInfo.CanWrite == false )
        throw new NotSupportedException("属性不支持写操做。");

    MethodInfo mi = propertyInfo.GetSetMethod(true);

    if( mi.GetParameters().Length > 1 )
        throw new NotSupportedException("不支持构造索引器属性的委托。");

    if( mi.IsStatic ) {
        Type instanceType = typeof(StaticSetterWrapper<>).MakeGenericType(propertyInfo.PropertyType);
        return (ISetValue)Activator.CreateInstance(instanceType, propertyInfo);
    }
    else {
        Type instanceType = typeof(SetterWrapper<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType);
        return (ISetValue)Activator.CreateInstance(instanceType, propertyInfo);
    }
}

  委托方案的后续问题

  前面的代码解决了属性的读写问题,然而使用它们还很不方便:每次都要建立一个ISetValue接口的实例,再调用它的方法。 其实这也是委托方案共有的问题:咱们须要为每一个属性的读写操做分别建立不一样的委托,并且委托太零散了。

  如何将属性与建立好的委托关联起来呢?(建立委托也是须要时间的)
  我想全部人都会想到用字典来保存。
  是的,好像也只有这一种方法了。
  为了提升性能,我改进了工厂类,缓存了包含委托的实例,
  为了方便使用前面的方法,我提供了一些扩展方法:

public static class GetterSetterFactory
{
    private static readonly Hashtable s_getterDict = Hashtable.Synchronized(new Hashtable(10240));
    private static readonly Hashtable s_setterDict = Hashtable.Synchronized(new Hashtable(10240));

    internal static IGetValue GetPropertyGetterWrapper(PropertyInfo propertyInfo)
    {
        IGetValue property = (IGetValue)s_getterDict[propertyInfo];
        if( property == null ) {
            property = CreatePropertyGetterWrapper(propertyInfo);
            s_getterDict[propertyInfo] = property;
        }
        return property;
    }

    internal static ISetValue GetPropertySetterWrapper(PropertyInfo propertyInfo)
    {
        ISetValue property = (ISetValue)s_setterDict[propertyInfo];
        if( property == null ) {
            property = CreatePropertySetterWrapper(propertyInfo);
            s_setterDict[propertyInfo] = property;
        }
        return property;
    }
}

public static class PropertyExtensions
{
    public static object FastGetValue(this PropertyInfo propertyInfo, object obj)
    {
        if( propertyInfo == null )
            throw new ArgumentNullException("propertyInfo");

        return GetterSetterFactory.GetPropertyGetterWrapper(propertyInfo).Get(obj);
    }

    public static void FastSetValue(this PropertyInfo propertyInfo, object obj, object value)
    {
        if( propertyInfo == null )
            throw new ArgumentNullException("propertyInfo");

        GetterSetterFactory.GetPropertySetterWrapper(propertyInfo).Set(obj, value);
    }
}

  说明:我在缓存的设计上并无使用泛型Dictionary,而是使用了Hashtable。
  我认可在简单的单线程测试中,Dictionary要略快于Hashtable 。

  再来测试一下FastSetValue的性能吧,毕竟大多数时候我会使用这个扩展方法。
  我又在测试代码中增长了一段:

propInfo.FastSetValue(testObj, 123);
Console.Write("FastSet花费时间:       ");
Stopwatch watch6 = Stopwatch.StartNew();

for( int i = 0; i < count; i++ )
    propInfo.FastSetValue(testObj, 123);

watch6.Stop();
Console.WriteLine(watch6.Elapsed.ToString());

  测试结果以下:

  测试结果代表:虽然通用接口ISetValue将反射性能优化了37倍,可是最终的FastSetValue将这个数字减小到还不到7倍(在CLR4中还不到5倍)。

  看到这个结果您是否也比较郁闷:优化了几十倍的结果,最后却丢了大头,只获得一个零头!

  中间那30倍的时间是哪里消耗了?
  1. Hashtable的查找时间。
  2. 代码的执行路径变长了。

  代码的执行路径变长了,我想全部人应该都能接受:为了简化调用并配合缓存一块儿工做,代码的执行路径确实变长了。

  Hashtable的查找时间应该很快吧? 您是否是也这样想呢?
  为了看看Hashtable的查找时间,我又加了一点测试代码:

Hashtable table = new Hashtable();
table[propInfo] = new object();
Console.Write("Hashtable花费时间:      ");
Stopwatch watch7 = Stopwatch.StartNew();

for( int i = 0; i < count; i++ ) {
    object val = table[propInfo];
}
watch7.Stop();
Console.WriteLine(watch7.Elapsed.ToString());

  如今运行测试代码的结果以下:

  确实,大部分时间消耗在Hashtable的查找上!

  缓存的线程并发问题

  集合不只仅只有查找开销,在多线程环境中,咱们还要考虑并发性。

  看到许多人作性能测试时,老是喜欢写个控制台程序,而后再来个for循环,执行多少万次!
  我认为 这样的结果只能反映代码在单线程环境下的性能,在多线程下,结果可能会有较大的差异, 固然了,多线程测试的确很复杂,也很可贵到准确的数字。 可是咱们的设计不能不考虑多线程下的并发问题。

  虽然我也在单线程环境下测试过Dictionary<TKey, TValue>的性能,的确要比Hashtable略好点。
  可是MSDN上对Dictionary的线程安全的描述是这样的:

  此类型的公共静态(在 Visual Basic 中为 Shared)成员是线程安全的。但不能保证任何实例成员是线程安全的。
  只要不修改该集合,Dictionary<(Of <(TKey, TValue>)>) 就能够同时支持多个阅读器。即使如此,从头至尾对一个集合进行枚举本质上并非一个线程安全的过程。当出现枚举与写访问互相争用这种极少发生的状况时,必须在整个枚举过程当中锁定集合。若要容许多个线程访问集合以进行读写操做,则必须实现本身的同步。

  而MSDN对Hashtable的线程安全的描述倒是:

  Hashtable 是线程安全的,可由多个读取器线程和一个写入线程使用。多线程使用时,若是只有一个线程执行写入(更新)操做,则它是线程安全的,从而容许进行无锁定的读取(若编写器序列化为 Hashtable)。若要支持多个编写器,若是没有任何线程在读取 Hashtable 对象,则对 Hashtable 的全部操做都必须经过 Synchronized 方法返回的包装完成。

  从头至尾对一个集合进行枚举本质上并非一个线程安全的过程。即便一个集合已进行同步,其余线程仍能够修改该集合,这将致使枚举数引起异常。若要在枚举过程当中保证线程安全,能够在整个枚举过程当中锁定集合,或者捕捉因为其余线程进行的更改而引起的异常。

  显然,二个集合都不能彻底支持多线程的并发读写。
  虽然Hashtable提供同步包装的线程安全版本,可是内部仍是在使用锁来保证同步的!
  没办法,在多线程环境中,任何复杂数据结构都有线程安全问题。

  如何保证集合在并发操做中数据的同步呢?
  是lock仍是ReaderWriterLock?
  显然前者的实现较为简单,因此它成了绝大多数人的首选。
  在.net4中,ConcurrentDictionary是另外一个新的首选方法。

  因为Dictionary只支持并发的读操做,因此只要涉及到写操做,它就不安全了,
  所以最安全地作法也只好在 读和写 操做上都加lock,不然就不安全了。

  而Hashtable则不一样,它的内部数据结构支持一个线程写入的同时容许多个线程并发读取,因此只要在写操做上加lock就能够实现线程同步, Hashtable的线程安全版本也就是这样实现的。 这也是我选择Hashtable的缘由。

  小结

  在这篇博客中,我演示了二种不一样的反射优化方法:
  1. 基于Emit的动态生成符合委托签名的IL代码。
  2. 使用Delegate.CreateDelegate直接建立委托。

  这是二种大相径庭的思路:
  1. Emit方法,须要先定义一个委托签名,而后生成符合委托签名的IL代码。
  2. CreateDelegate能够直接生成委托,但须要借用泛型类解决委托的类型参数问题,最后为了能通用,须要以接口方式调用强类型委托。

  虽然咱们可使用任何一种方法获得委托,可是咱们须要操做多少属性呢? 显然这是一个无解的问题,咱们只能为每一个属性建立不一样的委托。因此新的问题也随之产生: 咱们如何保存那些委托?如何让它们与属性关联起来? Dictionary或者Hashtable或许是较好的选择(.net 3.5),然而,这些对象内部的数据结构在查找时,并非零成本, 它们会消耗优化的大部分红果。 另外,在实现缓存委托的问题上,并发问题也是值得咱们考虑的,不高效的并发设计还会让优化的成果继续丢失!

  因此,我认为优化反射是个复杂问题,至少有3个环节是须要考虑的:   1. 如何获得委托?   2. 如何缓存委托?   3. 如何支持并发?

相关文章
相关标签/搜索