替代反射調用的幾種方式及性能測試


園子里和這個話題的相關文章比較多,本文是舊話重提,外加個小的總結。主要因為近期看到很多同事、朋友都已經使用 VS2012 進行 .NET 4.5 開發了,卻還在大量使用反射,不知道用新的方式。或有所了解,但又害怕性能不好不敢大膽去用。

本文以如下類為例:

1
2
3
4
5
6
7
public class MyMath
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

替代反射的幾種方式

倒序說吧,從最先進最簡單的開始。

1. dynamic 調用

.NET 4 引入了 dynamic 類型,可以使用如下方式來完成對 MyMath.Add 方法的動態調用:

1
2
dynamic math = new MyMath();
int result = math.Add(1, 2);

非常簡單,效率也不錯,可以看后面的性能對比測試結果。

但有一點要注意, dynamic 遵守 .NET 的訪問級別限定,會對成員進行可見性檢查。也就是說,只能 dynamic 調用 public 成員;當然,如果是同一程序集內部,internal 成員也是可以訪問的。

2. Expression Tree 編譯調用

Expression Tree 是 .NET 3.5 引入的。簡單地,我們可以使用 lambda 構建一顆 Expression Tree:

1
2
var math = new MyMath();
Expression<Func<int, int, int>> add = (a, b) => math.Add(a, b);

這種方法適合手工編碼構建,還有另外一種方式可以動態構建:

1
2
3
4
5
6
var add = typeof(MyMath).GetMethod("Add");
var math = Expression.Parameter(typeof(MyMath));
var a = Expression.Parameter(typeof(int), "a");
var b = Expression.Parameter(typeof(int), "b");
var body = Expression.Call(myMath, add, a, b);
var lambda = Expression.Lambda<Func<MyMath, int, int, int>>(body, math, a, b);

兩種方式構建出的 Tree 是相同的。

話歸正題,構建出表達式樹后,調用其 Compile 方法便可編譯成一個委托,如下代碼第 3 行:

1
2
3
4
var math = new MyMath();
Expression<Func<int, int, int>> addExpTree = (a, b) => math.Add(a, b);  // ExressionTree
Func<int, int, int> add = addExpTree.Compile();                         //  編譯成委托
var result = add(1, 2);                                                 // 相加,結果為3

與 dynamic 調用方法同,Expression Tree 編譯出的委托方法也遵守 .NET 的訪問級別限定,會對成員進行可見性檢查,不能訪問私有成員。

3. 反射發出調用

這里只介紹反射發出的一項技術 DynamicMethod,.NET 2.0 新增此類。

使用 DynamicMethod 類在運行時定義輕量全局方法,然后使用委托執行這些方法。

針對 MyMath.Add 方法,調用比前面兩種方式復雜些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var addMethod = typeof(MyMath).GetMethod("Add");
var dynamicMethod = new DynamicMethod("", typeof(int), new[] { typeof(MyMath), typeof(int), typeof(int) });
//
var il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Callvirt, addMethod);
il.Emit(OpCodes.Ret);
//
var add = (Func<MyMath, int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<MyMath, int, int, int>));
//
var math = new MyMath();
var result = add(math, 1, 2);

從第 5 行起,使用幾個 IL 匯編指令,簡單一說:

  • 第 5 行,OpCodes.Ldarg_0 是將索引為 0 的參數值推送到堆棧上,Ldarg_1、Ldarg_2 以此類推;
  • 第 6 行,OpCodes.Callvirt 是調用對象的(后期綁定)方法,並且將返回值推送到計算堆棧上;
  • 第 9 行,OpCodes.Ret 表達從當前方法返回,並將返回值(如果存在)從調用方的計算堆棧推送到被調用方的計算堆棧上。

反射發出是在匯編級別的,很底層,也就意味着效率更高、威力更強大。反射發出能繞過跳過 JIT 可見性檢查,訪問 private 成員(對於 DynamicMethod 類,請查看:DynamicMethod 構造函數 (String, Type, Type[], Boolean))。

下面是幾種方法的性能測試。

性能對比測試

這里對直接、反射發出、dynamic 、表達式樹編譯、反射五種調用方式進行性能對比測試。

測試結果

先給出測試的結果:

image

從上圖中可以看出:

  • 直接調用性能最佳;
  • 反射發出和表達式樹兩種方式性能相當,速度接近直接調用;
  • dynamic 性能居中,也不錯;
  • 反射方式性能最差。

另外說明兩點:

  1. 本次測試僅針對 MyMath.Add 方法,其參數和返回值都是值類型,反射調用時存在大量裝箱、拆箱。如果測試方法的參數和返回值都是引用類型,反射方式與其它方式間的差距會小些。
  2. 從上圖可以看出這幾次方式性能差別較大,但此結果是重復 100 萬次的情況下得出的。考慮單次調用,反射只比直接調用慢 381 納秒。如果你的代碼不是位於循環的中心或是系統的瓶頸,調用次數不多,性能差異可以完全忽略。

測試代碼

以下是測試用代碼,僅參考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using System;
using System.Linq.Expressions;
using System.Reflection.Emit;

public class MyMath {
    public int Add(int a, int b) { return a + b; }
}

class Program {
    static void Main(string[] args) {
        int result;
        var math = new MyMath();
        var count = 1000000;

        Console.WriteLine("數據量:" + count);
        Console.WriteLine("-----------------------------r\n");

        using (Profiler.Step("循環:{0} ms")) {
            for (int i = 0; i < count; i++)
                result = 1;
        }
        using (Profiler.Step("直接調用 :{0} ms")) {
            for (int i = 0; i < count; i++)
                result = math.Add(i, i);
        }
        using (Profiler.Step("反射發出:{0} ms")) {
            var emitAdd = BuildEmitAddFunc();
            for (int i = 0; i < count; i++)
                result = emitAdd(math, i, i);
        }
        using (Profiler.Step("表達式樹:{0} ms")) {
            var expressionAdd = BuildExpressionAddFunc();
            for (int i = 0; i < count; i++)
                result = expressionAdd(math, i, i);
        }
        using (Profiler.Step("dynamic 調用:{0} ms")) {
            dynamic d = math;
            for (int i = 0; i < count; i++)
                result = d.Add(i, i);
        }
        using (Profiler.Step("反射調用:{0} ms")) {
            var add = typeof(MyMath).GetMethod("Add");
            for (int i = 0; i < count; i++)
                result = (int)add.Invoke(math, new object[] { i, i });
        }

        Console.WriteLine("\r\n\r\n測試完成,任意鍵退出...");
        Console.ReadKey();
    }
    static Func<MyMath, int, int, int> BuildExpressionAddFunc() {
        var add = typeof(MyMath).GetMethod("Add");
        var math = Expression.Parameter(typeof(MyMath));
        var a = Expression.Parameter(typeof(int), "a");
        var b = Expression.Parameter(typeof(int), "b");
        var body = Expression.Call(math, add, a, b);
        var lambda = Expression.Lambda<Func<MyMath, int, int, int>>(body, math, a, b);
        return lambda.Compile();
    }
    static Func<MyMath, int, int, int> BuildEmitAddFunc() {
        var add = typeof(MyMath).GetMethod("Add");
        var dynamicMethod = new DynamicMethod("", typeof(int), new[] { typeof(MyMath), typeof(int), typeof(int) });
        var il = dynamicMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldarg_2);
        il.Emit(OpCodes.Callvirt, add);
        il.Emit(OpCodes.Ret);
        return (Func<MyMath, int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<MyMath, int, int, int>));
    }
}

Profiler 是我寫的一個類,用於簡化測試:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
using System.Diagnostics;

public class Profiler : IDisposable {
    private Stopwatch watch;
    private string message;

    private Profiler(string message) {
        this.watch = new Stopwatch();
        this.watch.Start();
        this.message = message;
    }

    public void Dispose() {
        watch.Stop();
        Console.WriteLine(message, watch.ElapsedMilliseconds);
        Console.WriteLine();
    }

    public static IDisposable Step(string message) {
        return new Profiler(message);
    }

    public static T Inline<T>(string message, Func<T> func) {
        using (new Profiler(message))
            return func();
    }
}

總結

綜上所述,我們可以使用 .NET 2.0 的 DynamicMethod,.NET 3.5 引入的 Expression Tree、.NET 4 新增的 dynamic 來替換反射調用,帶來更好的性能。

希望本文對大家有所幫助,也希望整天忙於項目、疲於工作的朋友抽點時間學習下 .NET Framework 的一些“新”特性。


免責聲明!

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



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