問題回顧
在上篇部落格中,我介紹了最佳化反射的第一個步驟:用委託調用代替直接反射調用。
然而,那隻是反射最佳化過程的開始,因為新的問題出現了:如何儲存大量的委託?
如果我們將委託儲存在字典集合中,會發現這種設計會浪費較多的執行時間,因為這種設計會引發三個新問題:
1. 代碼的執行路徑變長了。
2. 字典尋找是有成本開銷的。
3. 字典集合的並發讀寫需要鎖定,會影響並發性。
再來回顧一下上次的測試結果吧:
雖然通用介面ISetValue將反射效能最佳化了37倍,但是最終的FastSetValue將這個數字減少到還不到7倍(在CLR4中還不到5倍)。
難道您不覺得遺憾嗎?
再看看直接調用與反射調用的對比,它們的速度相差了上千倍!
能不能不使用委託?
既然委託最後引出了三個難以解決的問題,導致最佳化後速度比直接調用差距太遠,那我們能不能不使用委託呢?
委託調用並不是最佳化反射的唯一方案,我們還有其它方法,
之所以委託調用能成為常見的最佳化方案是因為它比較簡單。
假如我需要用用戶端提交的資料來填充某個資料對象,考慮到代碼的通用性,我會用反射寫成這樣:
/// <summary>/// 從HttpRequest載入obj所需的資料/// </summary>/// <param name="request"></param>/// <param name="obj"></param>public static void LoadDataFromHttpRequest(HttpRequest request, object obj){ PropertyInfo[] properties = obj.GetType().GetProperties(); foreach( PropertyInfo p in properties ) { // 這裡只是示意代碼,假設資料處理不會有異常。 object val = Convert.ChangeType(request[p.Name], p.PropertyType); p.FastSetValue(obj, val); }}
如果我事Crowdsourced Security Testing道要載入已知的資料類型,代碼會寫成這樣:
public static void LoadDataFromHttpRequest(HttpRequest request, OrderInfo order){ // 這裡只是示意代碼,假設資料處理不會有異常。 order.OrderID = int.Parse(request["OrderID"]); order.OrderDate = DateTime.Parse(request["OrderDate"]); order.SumMoney = decimal.Parse(request["SumMoney"]); order.Comment = request["Comment"]; order.Finished = bool.Parse(request["Finished"]);}
顯然,第二段代碼運行效率更快(儘管第一段代碼調用FastSetValue最佳化了速度)。
大家都知道反射效能較差,直接調用效能最好,那麼能不能在運行時不使用反射呢?
的確,使用反射是因為我們事先不知道要處理哪些類型的對象,因此不得不用反射,
另外,反射的代碼也更通用,寫一個方法可以載入所有的資料類型,可認為是一勞永逸的方法。
不過,就算我們事先不知道要處理哪些物件類型,但是只要使用反射,我們完全可以知道任何一個類型包含哪些資料成員,
還能知道這些資料成員的資料類型,這一點不用懷疑吧?
既然我們用反射可以知道所有的類型定義資訊,我們是否可以參照代碼產生器的思路去產生代碼呢?
我們可以參照前面第二段代碼,為【需要處理的類型】產生直接調用的代碼,這樣不就徹底解決了反射效能問題了嗎?
產生代碼的過程,其實也就是個字串的拼接過程,難度並不大,只是比較複雜而已。
如果前面的答案都是肯定的,那麼現在只有一個問題了:我們能在運行時執行拼接產生的字串代碼嗎?
答案也是肯定的:能!
CodeDOM:在運行時編譯代碼
回憶一下我們編寫的ASPX頁面,它們並不是C#代碼,它們本質上就是一個文字檔,
我們可以寫入一些HTML標籤,還有些標籤上加了 runat="server" 屬性,
我們還可以在頁面中插入一些C#程式碼片段,儘管它們不是我們編譯後的DLL檔案,然而它們就是運行起來了!
要知道ASP.NET不是ASP,ASP是解釋性的指令碼語言,而ASP.NET是以編譯方式啟動並執行,
所以,每個ASPX分頁檔最後都是運行編譯後的結果。
假設我有下面一段文本(文本的內容是一段C#代碼):
using System;using System.Collections.Generic;using System.Text;using System.Reflection;namespace OptimizeReflection{ public class DemoClass { public int Id { get; set; } public string Name; public int Add(int a, int b) { return a + b; } } public class 使用者手冊 { public static void Main() { // OptimizeReflection 這個類庫提供了一些擴充方法,它們用於最佳化常見的反射情境 // 下面是一些相關的示範樣本。 // 對於屬性的讀寫操作、方法的叫用作業,還提供了效能更好的強型別(泛型)版本,可參考Program.cs Type instanceType = typeof(DemoClass); PropertyInfo propertyInfo = instanceType.GetProperty("Id"); FieldInfo fieldInfo = instanceType.GetField("Name"); MethodInfo methodInfo = instanceType.GetMethod("Add"); // 1. 建立執行個體對象 DemoClass obj = (DemoClass)instanceType.FastNew(); // 2. 寫屬性 propertyInfo.FastSetValue(obj, 123); propertyInfo.FastSetValue2(obj, 123); // 3. 讀屬性 int a = (int)propertyInfo.FastGetValue(obj); int b = (int)propertyInfo.FastGetValue2(obj); // 4. 寫欄位 fieldInfo.FastSetField(obj, "Fish Li"); // 5. 讀欄位 string s = (string)fieldInfo.FastGetValue(obj); // 6. 調用方法 int c = (int)methodInfo.FastInvoke(obj, 1, 2); int d = (int)methodInfo.FastInvoke2(obj, 3, 4); Console.WriteLine("a={0}; b={1}; c={2}; d={3}; s={4}", a, b, c, d, s); } }}您可以把上面這段文本想像成前面第二個版本的LoadDataFromHttpRequest方法,如果我們在運行時使用反射也能產生那樣的代碼,
現在就差把它編譯成程式集了。下面的代碼示範了如何將一段文本編譯成程式集的過程:
string code = null;// 1. 產生要編譯的代碼。(樣本為了簡單直接從程式集內的資源中讀取)Stream stram = typeof(CodeDOM).Assembly .GetManifestResourceStream("TestOptimizeReflection.使用者手冊.txt");using( StreamReader sr = new StreamReader(stram) ) { code = sr.ReadToEnd();}//Console.WriteLine(code);// 2. 設定編譯參數,主要是指定將要引用哪些程式集CompilerParameters cp = new CompilerParameters();cp.GenerateExecutable = false;cp.GenerateInMemory = true;cp.ReferencedAssemblies.Add("System.dll");cp.ReferencedAssemblies.Add("OptimizeReflection.dll");// 3. 擷取編譯器並編譯代碼// 由於My Code使用了【自動屬性】特性,所以需要 C# .3.5版本的編譯器。// 擷取與CLR匹配版本的C#編譯器可以這樣寫:CodeDomProvider.CreateProvider("CSharp")Dictionary<string, string> dict = new Dictionary<string, string>();dict["CompilerVersion"] = "v3.5";dict["WarnAsError"] = "false";CSharpCodeProvider csProvider = new CSharpCodeProvider(dict);CompilerResults cr = csProvider.CompileAssemblyFromSource(cp, code);// 4. 檢查有沒有編譯錯誤if( cr.Errors != null && cr.Errors.HasErrors ) { foreach( CompilerError error in cr.Errors ) Console.WriteLine(error.ErrorText); return;}// 5. 擷取編譯結果,它是編譯後的程式集Assembly asm = cr.CompiledAssembly;整個過程分為5個步驟,它們已用注釋標識出來了,這裡不再重複了。
如何調用編譯結果前面的代碼把一段文本字串編譯成了程式集,現在還有最後一個問題:如何調用編譯結果?
答案:有二種方法,
1. 直接調用方法。
2. 執行個體化程式集中的類型,以介面方式調用方法。
其實這二種方法都需要使用反射,用反射定位到要調用的類型和方法。
第一種方法要求在產生代碼時,產生的類名和方法名是明確的,在調用方法時,我們有二個選擇:
1. 用反射的方式調用(這裡只是一次反射)。
2. 為方法產生委託(用上篇部落格介紹的方法),然後基於委託調用。
第二種方法要求在產生代碼時,首先要定義一個介面,保證產生的程式碼能實現指定的介面,
然而用反射找到要調用的類型名稱,用反射或者委託調用構造方法建立類型執行個體,最後基於介面去調用。
我們熟悉的ASPX頁面就是採用了這種方式來實現的。
這二種方法也可以這樣區分:
1. 如果產生的方法是靜態方法,應該選擇第一種方法。
2. 如果產生的方法是執行個體方法,那麼選擇第二種方法是合理的。
對於前面的樣本,我採用了第一種方法了,因為類名和方法名稱都是事先確定的而且實現起來比較簡單。
// 6. 找到目標方法,並調用Type t = asm.GetType("OptimizeReflection.使用者手冊");MethodInfo method = t.GetMethod("Main");method.Invoke(null, null);能不能不使用委託? 如何用好CodeDOM?
在這篇部落格中我不知道把它們安排在哪裡較為合適,算了,還是把答案留給下篇部落格吧。
部落格中所有代碼將在後續部落格中給出。
如果,您認為閱讀這篇部落格讓您有些收穫,不妨點擊一下右下角的【推薦】按鈕。
如果,您希望更容易地發現我的新部落格,不妨點擊一下右下角的【關注 Fish Li】。
因為,我的寫作熱情也離不開您的肯定支援。
感謝您的閱讀,如果您對我的部落格所講述的內容有興趣,請繼續關注我的後續部落格,我是Fish Li 。