優化反射性能的總結(中)


問題回顧

上篇博客中,我介紹了優化反射的第一個步驟:用委托調用代替直接反射調用。
然而,那只是反射優化過程的開始,因為新的問題出現了:如何保存大量的委托?

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

如果我事先知道要加載已知的數據類型,代碼會寫成這樣:

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. 獲取編譯器並編譯代碼
// 由於我的代碼使用了【自動屬性】特性,所以需要 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?
在這篇博客中我不知道把它們安排在哪里較為合適,算了,還是把答案留給下篇博客吧。

博客中所有代碼將在后續博客中給出。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM