都知道反射傷效能,但不得不反射的時候又怎麼辦呢?當真的被問題逼迫的時候還是能找到解決辦法的。
為反射得到的方法建立一個委託,此後呼叫此委託將能夠提高近乎直接呼叫方法本身的效能。(當然 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));
}
}
}
泛型的多引數版本可以使用泛型型別生成器生成,我在 生成程式碼,從 到 —— 自動生成多個型別的泛型 – 呂毅 一文中寫了一個泛型生成器,可以稍加修改以便適應這種泛型類。
朋友會在“發現-看一看”看到你“在看”的內容