反射是一種很重要的技術,然而它與直接調用相比性能要慢很多,因此如何優化反射性能也就成為一個不得不面對的問題。 目前最常見的優化反射性能的方法就是采用委托:用委托的方式調用需要反射調用的方法(或者屬性、字段)。
那么如何得到委托呢? 目前最常見也就是二種方法:Emit, ExpressionTree 。其中ExpressionTree可認為是Emit方法的簡化版本, 所以Emit是最根本的方法,它采用在運行時動態構造一段IL代碼來包裝需要反射調用的代碼, 這段動態生成的代碼滿足某個委托的簽名,因此最后可以采用委托的方式代替反射調用。
用Emit方法優化反射
如果我們需要設計自己的數據訪問層,那么就需要動態創建所有的數據實體對象,尤其是還要為每個數據實體對象的屬性賦值, 這里就要涉及用反射的方法對屬性執行寫操作,為了優化這種反射場景的性能,我們可以用下面的方法來實現:
我用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) 測試可以得到以下結果:
感謝zhangweiwen 在博客中展示了CRL 4.0對反射的性能改進, 在他的博客中還提供了一種采用表達式樹的優化版本,以及包含一個泛型的強類型的版本。
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. 如何支持並發?
得到委托是容易的,但它只是一個開始!
博客中所有代碼將在后續博客中給出。