最佳化反射效能的總結(中)

來源:互聯網
上載者:User
問題回顧

在上篇部落格中,我介紹了最佳化反射的第一個步驟:用委託調用代替直接反射調用。
然而,那隻是反射最佳化過程的開始,因為新的問題出現了:如何儲存大量的委託?

如果我們將委託儲存在字典集合中,會發現這種設計會浪費較多的執行時間,因為這種設計會引發三個新問題:
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 。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.