為什麼 .NET 的反射這麼慢?
大家都知道 .NET 的反射很慢,但是為什麼會出現這種情況呢?這篇文章會帶你尋找這個問題的真正原因。
CLR 型別系統的設計目標
原因之一是,在設計的時候反射本身就不是以高效能為目標的,可以參考Type System Overview – ‘Design Goals and Non-goals’(型別系統概覽 – ‘設計目標和非目標’):
目標
- 執行時通過快速執行(非反射)程式碼訪問需要的資訊。
- 編譯時直接訪問所需要的資訊來生成程式碼。
- 垃圾回收/遍歷棧可以訪問需要資訊而不需要鎖或分配記憶體。
- 一次只載入最少量的型別。
- 型別載入時只載入最少需要載入的型別。
- 型別系統的資料結構必須在 NGEN 映像中儲存。
非目標
- 後設資料的所有資訊能直接反射 CLR 資料結構。
- 快速使用反射。
參閱出處相同的 Type Loader Design – ‘Key Data Structures’(型別載入器設計 – ‘關鍵資料結構’):
EEClass
MethodTable(方法表)資料分為“熱”和“冷”兩種結構,以提高工作集和快取的利用率。MethodTable 本身只儲存程式穩定狀態的“熱”資料。EEClass 儲存“冷”資料,它們通常是型別載入、JITing或反射所需要的。每個 MethodTable 指向一個 EEClass。
反射是如何工作的?
我們已經知道反射本身就不是以快為目標來設計的,但是它為什麼需要那麼多時間呢?
為了說明這個問題,來看看反射呼叫過程中,託管程式碼和非託管程式碼的呼叫棧。
- System.Reflection.RuntimeMethodInfo.Invoke(..) - 原始碼連結
- 呼叫 System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..)
- System.RuntimeMethodHandle.PerformSecurityCheck(..) - 連結
- 呼叫 System.GC.KeepAlive(..)
- System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..) - 連結
- 呼叫 System.RuntimeMethodHandle.InvokeMethod(..) 的存根
- System.RuntimeMethodHandle.InvokeMethod(..) 的存根 - 連結
即使不點選連結,想必你也能直觀感受到改方法執行的大量程式碼。參考示例:System.RuntimeMethodHandle.InvokeMethodis 超過 400 行程式碼!
那麼,它具體在做什麼?
獲取方法資訊
要使用反射來呼叫欄位/屬性/方法,你必須獲得 FieldInfo/PropertyInfo/MethodInfo,使用這樣的程式碼:
Type t = typeof(Person); FieldInfo m = t.GetField("Name");
這需要一定的成本,因為需要提取相關的後設資料,並對其進行解析。執行時會幫我們維持一個內部快取,快取著所有欄位/屬性/方法。這個快取由 RuntimeTypeCache 類實現,用法示例在 RuntimeMethodInfo 類中.
執行 gist 中的程式碼你可以看到快取的何運作方式,它恰如其分地使用反射檢查執行時內部!
gist 上的程式碼會在你使用反射獲得 FieldInfo 之前輸出下列內容:
Type: ReflectionOverhead.Program Reflection Type: System.RuntimeType (BaseType: System.Reflection.TypeInfo) m_fieldInfoCache is null, cache has not been initialised yet
不過一旦你獲得欄位,就會輸出:
Type: ReflectionOverhead.Program Reflection Type: System.RuntimeType (BaseType: System.Reflection.TypeInfo) RuntimeTypeCache: System.RuntimeType+RuntimeTypeCache, m_cacheComplete = True, 4 items in cache [0] - Int32 TestField1 - Private [1] - System.String TestField2 - Private [2] - Int32 <TestProperty1>k__BackingField - Private [3] - System.String TestField3 - Private, Static
ReflectionOverhead.Program 看起來像這樣:
class Program { private int TestField1; private string TestField2; private static string TestField3; private int TestProperty1 { get; set; } }
看來執行時會篩選已經建立過的東西,這意味著呼叫 GetFeild 或 GetFields 不需要多大代價。對於 GetMethod 和 GetProperty 來說也是如此,MethodInfo 或 PropertyInfo 會在你第一次呼叫的時候建立並快取起來。
引數校驗和錯誤處理
得到 MethodInfo 之後,如果呼叫它的 Invoke 方法,會要處理很多事項。假設編寫程式碼如下:
PropertyInfo stringLengthField = typeof(string).GetProperty("Length", BindingFlags.Instance | BindingFlags.Public); var length = stringLengthField.GetGetMethod().Invoke(new Uri(), new object[0]);
如果執行上述程式碼,會得到下面的異常:
System.Reflection.TargetException: Object does not match target type. at System.Reflection.RuntimeMethodInfo.CheckConsistency(..) at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(..) at System.Reflection.RuntimeMethodInfo.Invoke(..) at System.Reflection.RuntimePropertyInfo.GetValue(..)
這是因為我們獲得了 String 類 Length 屬性的 PropertyInfo,但是卻在 Uri 物件上呼叫它,顯然,這是個錯誤的型別!
此外,你還必須在呼叫方法時對傳遞給方法的引數進行校驗。為了能傳遞引數,反射 API 使用了一個 object 的陣列作為引數,其中每一個元素表示一個引數。所以,如果你使用反射來呼叫 Add(int x, int y) 方法,你得呼叫 methodInfo.Invoke(.., new [] { 5, 6 })。執行時會對傳入引數的數量和型別進行檢查,在這個示例中你要確保是 2 個 int 型別的引數。這些工作不好的地方是常常需要裝箱,這會增加額外的成本。希望這在將來會降到最低。
安全性檢查
另一個主要任務是多重安全性檢查。例如,你不允許使用反射來任意呼叫你想呼叫的方法。這裡存在一些限制的或 ‘危險方法’,只能由可信度高的 .NET 框架程式碼呼叫。除了黑名單外,還有動態安全檢查,它由呼叫時必須檢查的的當前程式碼訪問安全許可權決定。
反射機制耗時多少?
瞭解反射的實際操作後,我們來看看實際耗時。請注意,這些基準測試是通過反射直接比較讀/寫屬性來完成的。在 .NET 中屬性是一對 Get/Set 方法,這是由編譯器生成的,但當屬性只包含一個簡單的內嵌欄位時,.NET JIT 會使用內聯 Get/Set 方法以提升效能。這意味著使用反射訪問屬性可能會遇到反射效能最差的情況,但它會被選擇是因為這是最常見的用例,資料位於 ORMs, Json 序列化/反序列化庫和物件對映工具中。
以下是由 BenchmarkDotNet 提供的原始結果,後面是在2個單獨的表中顯示的相同結果。 (全部Benchmark程式碼由此下載 )
讀取屬性值(‘Get’)
寫屬性值(‘Set’)
我們可以清楚地看到,正常的反射程式碼(GetViaReflection/SetViaReflection)比直接訪問屬性(GetViaProperty/SetViaProperty)要慢得多。 其他結果,我們還要進一步分析。
設定
首先我們從 aTestClass 開始,程式碼如下:
public class TestClass { public TestClass(String data) { Data = data; } private string data; private string Data { get { return data; } set { data = value; } } }
以及下面的通用程式碼,這裡包含了所有可用的選項:
// Setup code, done only once TestClass testClass = new TestClass("A String"); Type @class = testClass.GetType(); BindingFlag bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
正常的反射
首先我們使用常規基準程式碼來表示我們的起始情況和“最壞情況”:
[Benchmark]public string GetViaReflection() { PropertyInfo property = @class.GetProperty("Data", bindingFlags); return (string)property.GetValue(testClass, null); }
選擇1 – 快取 PropertyInfo
接下來,我們通過儲存引用至 PropertyInfo 以獲得速度上的少量提升,而不是每次都去獲取。但即使這樣,與直接訪問屬性相比,也仍然慢得多,這就表明在反射的“呼叫”部分成本很高。
// Setup code, done only once PropertyInfo cachedPropertyInfo = @class.GetProperty("Data", bindingFlags); [Benchmark] public string GetViaReflection() { return (string)cachedPropertyInfo.GetValue(testClass, null); }
選擇2 – 使用 FastMember
這裡使用了 Marc Gravell 優秀的 Fast Member 庫,這個庫用起來很簡單!
// Setup code, done only once TypeAccessor accessor = TypeAccessor.Create(@class, allowNonPublicAccessors: true); [Benchmark] public string GetViaFastMember() { return (string)accessor[testClass, "Data"]; }
注意,與其他選擇稍有不同,它建立了一個 TypeAccessor 來訪問型別中的所有屬性,而不僅是某一個。這帶來的負面影響是會導致執行時間變長,因為它在內部首先要為你請求的屬性(這個例子中是‘Data’)建立委託,然後再獲取其值。不過這種開銷是很小的,FastMember 仍然比其它反射方法更快,也更易用。所以我建議你先去看看。
這個選擇及隨後的選擇將反射程式碼轉換委託,這樣就可以直接呼叫而不再需要每次都進行反射,速度因此得到提升!
必須指出建立一個委託需要一定的成本(可以從 ‘相關閱讀’ 瞭解更多)。總之,速度提升是因為我們在其中進行過一次大投入(安全檢查等)並儲存了一個強型別的委託,之後我們只要稍微付出一點就可以一次次呼叫。如果反射只進行一次,那你大可不必使用這些技術。但是如果你只進行一次反射操作,它也不會出現效能瓶頸,你就完全不用在乎它會變慢!
通過委託讀某個屬性仍然不如直接訪問來得快,因為 .NET JIT 不會將對委託方法的呼叫進行內聯優化,而直接訪問屬性則會。因此即使使用委託,我們也需要為呼叫方法付出成本,而直接訪問屬性就不會。
選項3——建立代理(Delegate)
在這個選項中,我們使用 CreateDelegate 函式來將 PropertyInfo 轉換為常規的 delegate:
// Setup code, done only once PropertyInfo property = @class.GetProperty("Data", bindingFlags); Func<TestClass, string> getDelegate = (Func<TestClass, string>)Delegate.CreateDelegate( typeof(Func<TestClass, string>), property.GetGetMethod(nonPublic: true)); [Benchmark] public string GetViaDelegate() { return getDelegate(testClass); }
它的缺點是你必須知道編譯時的具體型別,也就是上面的程式碼中的 Func<TestClass,string> 部分(如果使用 Func<object,string>,編譯器會丟擲一個異常!)。不過,在大多數情況下,使用反射不會遇到這麼多麻煩。
有效避免麻煩,請參閱 MagicMethodHelper 程式碼(在 Jon Skeet 釋出的“Making Reflection fly and exploring delegates“部落格中),或閱讀下面的選項 4 或 5。
選項4——編譯表示式樹(Compiled Expression Trees)
這裡我們生成一個 delegate,但不同的是我們可以傳入一個 object,所以我們會看到“選項4”的限制。我們使用支援動態程式碼生成的 .NET Expression tree API:
// Setup code, done only once PropertyInfo property = @class.GetProperty("Data", bindingFlags); ParameterExpression = Expression.Parameter(typeof(object), "instance"); UnaryExpression instanceCast = !property.DeclaringType.IsValueType ? Expression.TypeAs(instance, property.DeclaringType) : Expression.Convert(instance, property.DeclaringType); Func<object, object> GetDelegate = Expression.Lambda<Func<object, object>>( Expression.TypeAs( Expression.Call(instanceCast, property.GetGetMethod(nonPublic: true)), typeof(object)), instance) .Compile(); [Benchmark] public string GetViaCompiledExpressionTrees() { return (string)GetDelegate(testClass); }
關於 Expression 的全部程式碼可以從“Faster Reflection using Expression Trees(使用表示式樹的快速反射機制)“部落格下載。
選項 5——IL Emit 動態程式碼生成
最後,雖然“權力越大,責任越大”,但這裡我們還是使用最底層的方法呼叫原始 IL,:
// Setup code, done only once PropertyInfo property = @class.GetProperty("Data", bindingFlags); Sigil.Emit getterEmiter = Emit<Func<object, string>> .NewDynamicMethod("GetTestClassDataProperty") .LoadArgument(0) .CastClass(@class) .Call(property.GetGetMethod(nonPublic: true)) .Return(); Func<object, string> getter = getterEmiter.CreateDelegate(); [Benchmark] public string GetViaILEmit() { return getter(testClass); }
使用 Expression tress(如選項 4 中所說),並沒有給出像直接呼叫 IL 程式碼那麼多的靈活性,儘管它確實能防止你呼叫無效程式碼! 考慮到這一點,如果你發現自己確實需要 emit IL,我強烈推薦你使用效能卓越的 Sigil 庫,因為它能在出錯時提供更好的錯誤提示訊息!
小結
如果(也只是如果)你發現自己在使用反射的時候有效能問題,有一些辦法可以讓它變得更快。獲得這些速度提升是因為委託帶來的對屬性/欄位/方法進行直接訪問,這避免了每次進行反射的開銷。
請在 /r/programming 和 /r/csharp 參考討論這篇文章
相關文章
- 為什麼反射慢?反射
- 你家的WiFi為什麼這麼慢?WiFi
- 為什麼Python這麼慢?Python
- 為什麼 Python 這麼慢?Python
- 淺談Using filesort和Using temporary 為什麼這麼慢
- Netty是什麼,Netty為什麼速度這麼快,執行緒模型分析Netty執行緒模型
- Nginx 為什麼這麼快?Nginx
- Redis為什麼這麼快?Redis
- 為什麼前端這麼多人前端
- 為什麼 JavaScript 的 this 要這麼用?JavaScript
- 我的sql沒問題為什麼還是這麼慢|MySQL加鎖規則MySql
- 什麼是反射?反射
- 反射是什麼反射
- 為什麼redis是單執行緒的以及為什麼這麼快?Redis執行緒
- 快速排序為什麼這麼快?排序
- IPP SWAP】為什麼這麼火爆 ||
- 為什麼 Laravel 這麼優秀Laravel
- CSS 為什麼這麼難學?CSS
- webpack 為什麼這麼難用?Web
- 為什麼 CSS 這麼難學?CSS
- 為什麼 Python 這麼火Python
- 雲列印為什麼這麼便宜?
- python有什麼特性?為什麼這麼火?Python
- Python是什麼?為什麼這麼搶手?Python
- 為什麼要用Redis?Redis為什麼這麼快?(來自知乎)Redis
- 為什麼你寫的Python執行的那麼慢呢?Python
- 在.net中為什麼第一次執行會慢?
- Python能幹什麼?為什麼會這麼火?Python
- 今年找工作為什麼這麼難?
- 為什麼凸優化這麼重要?優化
- 為什麼Julia語言這麼棒?
- Spring Boot 為什麼這麼火?Spring Boot
- 為什麼HTML5這麼火?HTML
- 解析:Python為什麼這麼流行?Python
- 為什麼有這麼多 Python?Python
- Exadata為什麼這麼牛B
- 不知道為什麼,mysql速度變慢MySql
- 什麼是Python?Python為什麼這麼搶手?Python