都知道反射傷效能,但不得不反射的時候又怎麼辦呢?當真的被問題逼迫的時候還是能找到解決辦法的。
為反射得到的方法建立一個委託,此後呼叫此委託將能夠提高近乎直接呼叫方法本身的效能。(當然 Emit 也能夠幫助我們顯著提升效能,不過直接得到可以呼叫的委託不是更加方便嗎?)
效能對比資料

▲ 沒有什麼能夠比資料更有說服力(註意後面兩行是有秒數的)
可能我還需要解釋一下那五行資料的含義:
- 直接呼叫(?應該沒有什麼比直接呼叫函式本身更有效能優勢的吧)
- 做一個跟直接呼叫的方法功能一模一樣的委託(?目的是看看呼叫委託相比呼叫方法本身是否有效能損失,從資料上看,損失非常小)
- 本文重點 將反射出來的方法建立一個委託,然後呼叫這個委託(?看看吧,效能跟直接調差別也不大嘛)
- 先反射得到方法,然後一直呼叫這個方法(?終於可以看出來反射本身還是挺傷效能的了,50 多倍的效能損失啊)
- 快取都不用,從頭開始反射然後呼叫得到的方法(?100 多倍的效能損失了)
以下是測試程式碼,可以更好地理解上圖資料的含義:
using System;
using System.Diagnostics;
using System.Reflection;
namespace Walterlv.Demo
{
public class Program
{
static void Main(string[] args)
{
// 呼叫的標的實體。
var instance = new StubClass();
// 使用反射找到的方法。
var method = typeof(StubClass).GetMethod(nameof(StubClass.Test), new[] { typeof(int) });
// 將反射找到的方法建立一個委託。
var func = InstanceMethodBuilder.CreateInstanceMethod(instance, method);
// 跟被測方法功能一樣的純委託。
Func pureFunc = value => value;
// 測試次數。
var count = 10000000;
// 直接呼叫。
var watch = new Stopwatch();
watch.Start();
for (var i = 0; i < count; i++)
{
var result = instance.Test(5);
}
watch.Stop();
Console.WriteLine($"{watch.Elapsed} - {count} 次 - 直接呼叫");
// 使用同樣功能的 Func 呼叫。
watch.Restart();
for (var i = 0; i < count; i++)
{
var result = pureFunc(5);
}
watch.Stop();
Console.WriteLine($"{watch.Elapsed} - {count} 次 - 使用同樣功能的 Func 呼叫");
// 使用反射創建出來的委託呼叫。
watch.Restart();
for (var i = 0; i < count; i++)
{
var result = func(5);
}
watch.Stop();
Console.WriteLine($"{watch.Elapsed} - {count} 次 - 使用反射創建出來的委託呼叫");
// 使用反射得到的方法快取呼叫。
watch.Restart();
for (var i = 0; i < count; i++)
{
var result = method.Invoke(instance, new object[] { 5 });
}
watch.Stop();
Console.WriteLine($"{watch.Elapsed} - {count} 次 - 使用反射得到的方法快取呼叫");
// 直接使用反射呼叫。
watch.Restart();
for (var i = 0; i < count; i++)
{
var result = typeof(StubClass).GetMethod(nameof(StubClass.Test), new[] { typeof(int) })
?.Invoke(instance, new object[] { 5 });
}
watch.Stop();
Console.WriteLine($"{watch.Elapsed} - {count} 次 - 直接使用反射呼叫");
}
private class StubClass
{
public int Test(int i)
{
return i;
}
}
}
}
上面的程式碼中,有一個我們還沒有實現的 InstanceMethodBuilder 型別,接下來將介紹如何實現它。
如何實現
實現的關鍵就在於 MethodInfo.CreateDelegate 方法。這是 .NET Standard 中就有的方法,這意味著 .NET Framework 和 .NET Core 中都可以使用。
此方法有兩個多載:
- 要求傳入一個型別,而這個型別就是應該轉成的委託的型別
- 要求傳入一個型別和一個實體,一樣的,型別是應該轉成的委託的型別
他們的區別在於前者創建出來的委託是直接呼叫那個實體方法本身,後者則更原始一些,真正呼叫的時候還需要傳入一個實體物件。
拿上面的 StubClass 來說明會更直觀一些:
private class StubClass
{
public int Test(int i)
{
return i;
}
}
前者得到的委託相當於 int Test(int i) 方法,後者得到的委託相當於 int Test(StubClass instance, int i) 方法。(在 IL 裡實體的方法其實都是後者,而前者更像 C# 中的程式碼,容易理解。)
單獨使用 CreateDelegate 方法可能每次都需要嘗試第一個引數到底應該傳入些什麼,於是我將其封裝成了泛型版本,增加易用性。
using System;
using System.Linq;
using System.Reflection;
using System.Diagnostics.Contracts;
namespace Walterlv.Demo
{
public static class InstanceMethodBuilder
{
///
/// 呼叫時就像 var result = func(t)。
///
[Pure]
public static Func CreateInstanceMethod(TInstanceType instance, MethodInfo method)
{
if (instance == null) throw new ArgumentNullException(nameof(instance));
if (method == null) throw new ArgumentNullException(nameof(method));
return (Func) method.CreateDelegate(typeof(Func), instance);
}
///
/// 呼叫時就像 var result = func(this, t)。
///
[Pure]
public static Func CreateMethod(MethodInfo method)
{
if (method == null)
throw new ArgumentNullException(nameof(method));
return (Func) method.CreateDelegate(typeof(Func));
}
}
}
泛型的多引數版本可以使用泛型型別生成器生成,我在 生成程式碼,從 到 —— 自動生成多個型別的泛型 – 呂毅 一文中寫了一個泛型生成器,可以稍加修改以便適應這種泛型類。
知識星球
朋友會在“發現-看一看”看到你“在看”的內容