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. 如何支持併發?

相關文章
相關標籤/搜索