歡迎光臨
每天分享高質量文章

.NET Core/Framework 建立委託以大幅度提高反射呼叫的效能

都知道反射傷效能,但不得不反射的時候又怎麼辦呢?當真的被問題逼迫的時候還是能找到解決辦法的。

為反射得到的方法建立一個委託,此後呼叫此委託將能夠提高近乎直接呼叫方法本身的效能。(當然 Emit 也能夠幫助我們顯著提升效能,不過直接得到可以呼叫的委託不是更加方便嗎?)


效能對比資料


▲ 沒有什麼能夠比資料更有說服力(註意後面兩行是有秒數的)

可能我還需要解釋一下那五行資料的含義:

  1. 直接呼叫(?應該沒有什麼比直接呼叫函式本身更有效能優勢的吧)
  2. 做一個跟直接呼叫的方法功能一模一樣的委託(?目的是看看呼叫委託相比呼叫方法本身是否有效能損失,從資料上看,損失非常小)
  3. 本文重點 將反射出來的方法建立一個委託,然後呼叫這個委託(?看看吧,效能跟直接調差別也不大嘛)
  4. 先反射得到方法,然後一直呼叫這個方法(?終於可以看出來反射本身還是挺傷效能的了,50 多倍的效能損失啊)
  5. 快取都不用,從頭開始反射然後呼叫得到的方法(?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));
}
}
}

泛型的多引數版本可以使用泛型型別生成器生成,我在 生成程式碼,從  到  —— 自動生成多個型別的泛型 – 呂毅 一文中寫了一個泛型生成器,可以稍加修改以便適應這種泛型類。

已同步到看一看
贊(0)

分享創造快樂