いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

リフレクションの動的実行時のパフォーマンスを上げる方法について (System.Reflection, MethodInfo.Invoke, Delegates, Late Binding, Dynamic Invocations, 速度)


C# 4.0 in a Nutshell
C# 4.0 IN A NUTSHELLに書いてあったのでとりあえずメモメモ。
実際にやってみたら、結構な差がでました。


リフレクションを利用する際に気になるのがパフォーマンスです。
当然、レイトバインディングを利用する事になるので通常よりも遅くなります。
それでも、1度きりの呼び出しとか複数回程度なら通常問題にならないです。
問題になるのは、ループ内とかで非常に多数の呼び出しが行われる際とかです。


実際極端な例ですが

            //
            // MethodInfo.Invokeを利用するパターン.
            //
            MethodInfo mi = typeof(string).GetMethod("Trim", new Type[0]);

            Stopwatch watch = Stopwatch.StartNew();
            for (int i = 0; i < 3000000; i++)
            {
                string result = mi.Invoke("test", null) as string;
            }
            watch.Stop();

            Console.WriteLine("MethodInfo.Invokeを直接呼ぶ: {0}", watch.Elapsed);
            
            //
            // 本来のメソッドを直接呼ぶパターン.
            //
            watch.Reset();
            watch.Start();
            for (int i = 0; i < 3000000; i++)
            {
            	string result = "test".Trim();
            }
            watch.Stop();

            Console.WriteLine("string.Trimを直接呼ぶ: {0}", watch.Elapsed);

というサンプルを実行した場合、私の環境では以下のようになりました。

MethodInfo.Invokeを直接呼ぶ: 00:00:03.6433016
string.Trimを直接呼ぶ: 00:00:00.0651544


当然直接呼ぶのが圧倒的に速いです。(当たり前ですが)
で、ここで

MethodInfoをDelegateにして実行

という方法をとると、大分速くなります。


何故かというとMethodInfoを直接呼ぶ場合、毎回レイトバインディングされている
状態ですが、Delegateを構築して呼ぶ場合、レイトバインディングが発生するのは
Delegateを構築する最初の一回のみです。後はDelegateを呼んでいるだけです。


以下、サンプルです。

    #region ReflectionSample-03
    public class ReflectionSample03 : IExecutable
    {
        delegate string StringToString(string s);
        
        public void Execute()
        {
            //
            // リフレクションを利用して処理を実行する場合
            // そのままMethodInfoのInvokeを呼んでも良いが
            // 何度も呼ぶ必要がある場合、以下のように一旦delegateに
            // してから実行する方が、パフォーマンスが良い。
            //
            // MethodInfo.Invokeを直接呼ぶパターンでは、毎回レイトバインディング
            // が発生しているが、delegateにしてから呼ぶパターンでは
            // delegateを構築している最初の一回のみレイトバインディングされるからである。
            //
            // 尚、当然一番速いのは本来のメソッドを直接呼ぶパターン。
            //
            
            //
            // MethodInfo.Invokeを利用するパターン.
            //
            MethodInfo mi = typeof(string).GetMethod("Trim", new Type[0]);

            Stopwatch watch = Stopwatch.StartNew();
            for (int i = 0; i < 3000000; i++)
            {
                string result = mi.Invoke("test", null) as string;
            }
            watch.Stop();

            Console.WriteLine("MethodInfo.Invokeを直接呼ぶ: {0}", watch.Elapsed);

            //
            // Delegateを構築して呼ぶパターン.
            //
            StringToString s2s = (StringToString) Delegate.CreateDelegate(typeof(StringToString), mi);
            watch.Reset();
            watch.Start();
            for (int i = 0; i < 3000000; i++)
            {
                string result = s2s("test");
            }
            watch.Stop();

            Console.WriteLine("Delegateを構築して処理: {0}", watch.Elapsed);

            //
            // 本来のメソッドを直接呼ぶパターン.
            //
            watch.Reset();
            watch.Start();
            for (int i = 0; i < 3000000; i++)
            {
            	string result = "test".Trim();
            }
            watch.Stop();

            Console.WriteLine("string.Trimを直接呼ぶ: {0}", watch.Elapsed);
        }
    }
    #endregion


結果は以下のようになりました。

MethodInfo.Invokeを直接呼ぶ: 00:00:03.6433016
Delegateを構築して処理: 00:00:00.0730222
string.Trimを直接呼ぶ: 00:00:00.0651544


今回のサンプルでは、本来のメソッド呼び出しと大差ない
結果になりました。もちろんどのメソッドもこのようになるとは
限りませんが、有効な手立てだと思います。


================================
過去の記事については、以下のページからご参照下さい。