1. 程式人生 > >反射性能優化2

反射性能優化2

步驟 tee blog tin 回憶 ext 一段 stat nis

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

  如果我們將委托保存在字典集合中,會發現這種設計會浪費較多的執行時間,因為這種設計會引發三個新問題:
  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?
  在這篇博客中我不知道把它們安排在哪裏較為合適,算了,還是把答案留給下篇博客吧。

反射性能優化2