反射提供強大的動態能力,但效能代價高昂。本文呈實測數據:直接呼叫 vs 反射呼叫在建構子、實例方法、靜態方法三場景的效能差距,並分析 IL 層面根本原因。
// 測試類別
public class CTester
{
private double _a = 10;
public void Test1() => _a = (_a - 0.0001) * 1.0001;
public double GetA() => _a;
}
// 測試參數:1000 外層 × 100 內層 = 100,000 次呼叫
const int OUTER = 1000;
const int INNER = 100;
DateTime now = DateTime.Now;
for (int i = 0; i < OUTER; i++)
for (int j = 0; j < INNER; j++)
var obj = new CTester();
TimeSpan direct = DateTime.Now - now; // ~16 ms
DateTime now = DateTime.Now;
for (int i = 0; i < OUTER; i++)
for (int j = 0; j < INNER; j++)
{
Type t = Type.GetType("ReflectionTest.Test.CTester");
var obj = t.InvokeMember(null, BindingFlags.CreateInstance, null, null, null);
}
TimeSpan reflect = DateTime.Now - now; // ~5,520 ms (350x)
Type t = Type.GetType("ReflectionTest.Test.CTester"); // 提前取得
DateTime now = DateTime.Now;
for (int i = 0; i < OUTER; i++)
for (int j = 0; j < INNER; j++)
var obj = t.InvokeMember(null, BindingFlags.CreateInstance, null, null, null);
TimeSpan reflectOptimized = DateTime.Now - now; // ~1,332 ms (20x)
var obj = new CTester();
DateTime now = DateTime.Now;
for (int i = 0; i < OUTER; i++)
for (int j = 0; j < INNER; j++)
obj.Test1();
TimeSpan direct = DateTime.Now - now; // ~10 ms
Type t = Type.GetType("ReflectionTest.Test.CTester");
var obj = t.InvokeMember(null, BindingFlags.CreateInstance, null, null, null);
DateTime now = DateTime.Now;
for (int i = 0; i < OUTER; i++)
for (int j = 0; j < INNER; j++)
t.InvokeMember("Test1", BindingFlags.InvokeMethod, null, obj, null);
TimeSpan reflect = DateTime.Now - now; // ~2,500 ms (250x)
// 直接呼叫 ~5 ms
// 反射呼叫 ~3,164 ms (600x+)
| 操作 | 直接呼叫 | 反射 (未優化) | 反射 (快取 Type) | 倍數差距 (未優化) | 倍數差距 (快取) |
|---|---|---|---|---|---|
| 建構子 | 16 ms | 5,520 ms | 1,332 ms | 350x | 20x |
| 實例方法 | 10 ms | 2,500 ms | — | 250x | — |
| 靜態方法 | 5 ms | 3,164 ms | — | 630x | — |
// newobj 直接配置記憶體並呼叫建構子
IL_0001: newobj instance void CTester::.call()
// callvirt 直接呼叫實例方法
IL_0007: callvirt instance void CTester::Test1()
// 複雜的反射呼叫鏈
IL_0001: ldtoken ReflectionTest.Test.CTester
IL_0006: call Type.GetTypeFromHandle
IL_000B: ldstr "Test1"
IL_0010: ldc.i4.s 16 (BindingFlags.InvokeMethod)
IL_0012: ldnull
IL_0013: ldloc.0 (obj)
IL_0014: ldnull
IL_0015: callvirt Type.InvokeMember(string, BindingFlags, Binder, object, object[])
核心差異:callvirt 直接跳轉到方法入口點;InvokeMember 經過完整反射解析流程:
| 策略 | 適用場景 | 效能提升 | 複雜度 |
|---|---|---|---|
| 快取 Type/MethodInfo/PropertyInfo | 重複反射同一成員 | 10-20x | 低 |
| Delegate.CreateDelegate | 固定目標方法,高頻呼叫 | 接近直接呼叫 | 中 |
| Expression Tree 編譯 Lambda | 動態建構邏輯,重複執行 | 接近直接呼叫 | 中高 |
| DynamicMethod + ILGenerator | 極致效能需求 | 接近直接呼叫 | 高 |
| Source Generators (C# 9+) | 編譯期產生程式碼替代反射 | 零執行期反射 | 中 |
// 一次性建立委派
MethodInfo method = typeof(CTester).GetMethod("Test1");
var action = (Action)Delegate.CreateDelegate(typeof(Action), method);
// 高頻呼叫:效能接近直接呼叫
for (int i = 0; i < 100000; i++)
action(obj);
反射效能成本極高 (20x-600x),核心在於 InvokeMember 的動態解析開銷。熱點路徑嚴禁使用反射。必須使用時:快取 MemberInfo、改用 Delegate.CreateDelegate、或採用原始碼產生/Expression Tree 替代。架構設計階段應評估動態性需求 vs 效能預算,優先選擇強型別、編譯期綁定方案。