さまざまな方法でのメソッド呼び出し速度の調査

長いテキストが気に入らない人のための結果と結論





100,000回の呼び出し、テストの20回の繰り返し、x86 100,000回の呼び出し、テストの20回の繰り返し、x64 1,000,000回の呼び出し、10回のテスト反復、x86 1,000,000回の呼び出し、10回のテスト反復、x64
ダイレクトコール
最小:1ミリ秒
最大:1ミリ秒
平均:1ミリ秒
中央値:1ミリ秒
腹筋:1


最小:1ミリ秒
最大:1ミリ秒
平均:1ミリ秒
中央値:1ミリ秒
腹筋:1


最小:7ミリ秒
最大:8ミリ秒
平均:7.5ミリ秒
中央値:7.5ミリ秒
腹筋:1


最小:5ミリ秒
最大:6ミリ秒
平均:5.2ミリ秒
中央値:5ミリ秒
腹筋:1


リフレクションを介して呼び出します
最小:32ミリ秒
最大:36ミリ秒
平均:32.75ミリ秒
中央値:32.5 ms
リリース:32


最小:35ミリ秒
最大:44ミリ秒
平均:36.5ミリ秒
中央値:36ミリ秒
リリース:36


最小:333ミリ秒
最大:399ミリ秒
平均:345.5 ms
中央値:338ミリ秒
リリース:45


最小:362ミリ秒
最大:385ミリ秒
平均:373.6 ms
中央値:376ミリ秒
リリース:75


デリゲートコール
最小:64ミリ秒
最大:71ミリ秒
平均:65.05ミリ秒
中央値:64.5ミリ秒
リリース:64


最小:72ミリ秒
最大:86ミリ秒
平均:75.95ミリ秒
中央値:75ミリ秒
リリース:75


最小:659ミリ秒
最大:730ミリ秒
平均:688.8ミリ秒
中央値:689.5ミリ秒
リリース:92


最小:746ミリ秒
最大:869ミリ秒
平均:773.4ミリ秒
中央値:765ミリ秒
リリース:153


最適化されたデリゲートを介した呼び出し
最小:16ミリ秒
最大:18ミリ秒
平均:16.2 ms
中央値:16ミリ秒
リリース:16


最小:21ミリ秒
最大:25ミリ秒
平均:22.15ミリ秒
中央値:22ミリ秒
リリース:22


最小:168ミリ秒
最大:187ミリ秒
平均:172.8ミリ秒
中央値:170.5ミリ秒
リリース:22.7


最小:218ミリ秒
最大:245ミリ秒
平均:228.8ミリ秒
中央値:227ミリ秒
リリース:45.4


動的な呼び出し
最小:11ミリ秒
最大:14ミリ秒
平均:11.5ミリ秒
中央値:11ミリ秒
リリース:11


最小:12ミリ秒
最大:14ミリ秒
平均:12.5ミリ秒
中央値:12ミリ秒
リリース:12


最小:124ミリ秒
最大:147ミリ秒
平均:132.1ミリ秒
中央値:130ミリ秒
リリース:17


最小:127ミリ秒
最大:144ミリ秒
平均:131.5ミリ秒
中央値:129.5ミリ秒
リリース:26


式を介した呼び出し
最小:4ミリ秒
最大:4ミリ秒
平均:4ミリ秒
中央値:4ミリ秒
リリース:4


最小:4ミリ秒
最大:5ミリ秒
平均:4.15ミリ秒
中央値:4ミリ秒
リリース:4


最小:46ミリ秒
最大:55ミリ秒
平均:50ミリ秒
中央値:50.5ミリ秒
リリース:6.7


最小:47ミリ秒
最大:51ミリ秒
平均:47.7ミリ秒
中央値:47ミリ秒
リリース:9.4






.NET Framework 3.5を使用する場合は、呼び出し最適化を使用してデリゲートを介してメソッド呼び出しを使用することをお勧めします。 .NET Framework 4.0+の場合、ダイナミックを使用するのが最適です。

UPD: mayorovpからの新しい出力: Expressionを使用するのが最適です



UPD:そして、 CdEmONが促したように、そのような研究は以前にhabrで公開されました







少しオフトピック、研究の理由について
次のコードを記述します。

class SampleGeneric<T> { public long Process(T obj) { return String.Format("{0} [{1}]", obj.ToString(), obj.GetType().FullName).Length; } } class Container { private static Dictionary<Type, object> _instances = new Dictionary<Type, object>(); public static void Register<T>(SampleGeneric<T> instance) { if (false == _instances.ContainsKey(typeof(T))) { _instances.Add(typeof(T), instance); } else { _instances[typeof(T)] = instance; } } public static SampleGeneric<T> Get<T>() { if (false == _instances.ContainsKey(typeof(T))) throw new KeyNotFoundException(); return (SampleGeneric<T>)_instances[typeof(T)]; } public static object Get(Type type) { if (false == _instances.ContainsKey(type)) throw new KeyNotFoundException(); return _instances[type]; } }
      
      







同様のコードは非常に頻繁に使用されますが、欠点が1つあります。C#では、ジェネリック型のコレクションを明示的に保存できません。 私が見つけたすべてのアドバイスは、基本的な非ジェネリッククラス、インターフェイス、または抽象クラスを強調表示することになります。これらはストレージ用に示されます。 つまり このようなものが得られます:

 public interface ISampleGeneric { } class SampleGeneric<T> : ISampleGeneric // private static Dictionary<Type, ISampleGeneric> _instances = new Dictionary<Type, ISampleGeneric>();
      
      







私の意見では、次のように書く機能を追加すると便利です。

 //  Type expected Dictionary<Type, SampleGeneric<>>
      
      





特に、リフレクションを介してジェネリック型を作成する場合、同様の構造を使用することを考慮してください。

 typeof(SampleGeneric<>).MakeGenericType(typeof(string))
      
      





しかし、問題に戻ります。 ここで、特定のインスタンスを取得する必要があるが、非ジェネリックメソッドで取得する必要があると想像してください。 たとえば、オブジェクトを受け入れ、そのタイプに基づいてメソッドはハンドラーを選択する必要があります。

 void NonGenericMethod(object obj) { var handler = Container.Get(obj.GetType()); }
      
      







ハンドラーを取得しますが、現在環境ではhandler.Process(obj)の書き込みが許可されておらず、書き込みを行うと、コンパイラーはそのようなメソッドがないことを誓います。

ここでも、次のようなC#開発者によるデザインがあります。

 Container.GetInstance<fromtype(obj.GetType())>().Process(obj);
      
      





、しかしそれは存在せず、メソッドを呼び出す必要があります(Roslynを考慮したとしても、新しいIDEで既に同様にできますか?)。 これを行うには多くの方法があり、そのうちいくつかの主要な方法を区別できます。 それらは記事の冒頭の表にリストされています。





コードについて



以下は、スポイラーで指定されたコード呼び出しです。 時間測定はコードから削除され、測定はストップウォッチで行われました。 分析のために、絶対ではなく相対実行時間に興味があったので、鉄やその他のパラメーターは重要ではありません。 異なるPCでテストした結果は似ています。

また、呼び出しではプリプロセス時間を考慮に入れていないことに注意してください。プリプロセス時間では、目的のメソッドに到達します。 実際の条件、高負荷のタスクでは、このようなアクションは1回だけ実行され、結果はキャッシュされるため、この時間は分析では問題になりません。



ダイレクトコール




メソッドを直接プルするだけです。 表では、最初の行の直接呼び出しの結果であるAbs値は、常に1です。残りの行では、メソッド呼び出しの他のメソッド(Rel値)の呼び出しの速度が低下していることがわかります。

 public static TestResult TestDirectCall(DateTime arg) { var instance = Container.Get<DateTime>(); long summ = 0; for (long i = 0; i < ITERATION_COUNT; i++) { summ += instance.Process(arg); } // return }
      
      







リフレクションを介して呼び出します




そもそも使いたい最も簡単で手頃な方法です。 メソッドテーブルからメソッドを取得し、Invokeを介してプルします。 同時に、最も遅い方法の1つです。

 public static TestResult TestReflectionCall(object arg) { var instance = Container.Get(arg.GetType()); var method = instance.GetType().GetMethod("Process"); long summ = 0; for (long i = 0; i < ITERATION_COUNT; i++) { summ += (long)method.Invoke(instance, new object[] { arg }); } // return }
      
      







デリゲートおよび追加の最適化を使用したデリゲートを介した呼び出し




デリゲートを作成するためのコード

 private static Delegate CreateDelegate(object target, MethodInfo method) { var methodParameters = method.GetParameters(); var arguments = methodParameters.Select(d => Expression.Parameter(d.ParameterType, d.Name)).ToArray(); var instance = target == null ? null : Expression.Constant(target); var methodCall = Expression.Call(instance, method, arguments); return Expression.Lambda(methodCall, arguments).Compile(); }
      
      







したがって、テストコードは次のようになります。

 public static TestResult TestDelegateCall(object arg) { var instance = Container.Get(arg.GetType()); var hook = CreateDelegate(instance, instance.GetType().GetMethod("Process")); long summ = 0; for (long i = 0; i < ITERATION_COUNT; i++) { summ += (long)hook.DynamicInvoke(arg); } // return }
      
      





Reflectionメソッドに比べて2倍遅くなりますが、このメソッドをスローできますが、プロセスを高速化する素晴らしい方法があります。 正直なところ、私はImpromptuプロジェクト、つまりこの場所で彼をスパイしました。



デリゲートコール最適化コード
 internal static object FastDynamicInvokeDelegate(Delegate del, params dynamic[] args) { dynamic tDel = del; switch (args.Length) { default: try { return del.DynamicInvoke(args); } catch (TargetInvocationException ex) { throw ex.InnerException; } #region Optimization case 1: return tDel(args[0]); case 2: return tDel(args[0], args[1]); case 3: return tDel(args[0], args[1], args[2]); case 4: return tDel(args[0], args[1], args[2], args[3]); case 5: return tDel(args[0], args[1], args[2], args[3], args[4]); case 6: return tDel(args[0], args[1], args[2], args[3], args[4], args[5]); case 7: return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); case 8: return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); case 9: return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); case 10: return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); case 11: return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10]); case 12: return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11]); case 13: return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12]); case 14: return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13]); case 15: return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14]); case 16: return tDel(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15]); #endregion } }
      
      









テストコードを少し変更する



 public static TestResult TestDelegateOptimizeCall(object arg) { var instance = Container.Get(arg.GetType()); var hook = CreateDelegate(instance, instance.GetType().GetMethod("Process")); long summ = 0; for (long i = 0; i < ITERATION_COUNT; i++) { summ += (long)FastDynamicInvokeDelegate(hook, arg); } // return }
      
      







また、通常のデリゲート呼び出しと比較して10倍の加速が得られます。 現時点では、これは考慮されているものの中で最良のオプションです。



動的な呼び出し




そして、メインキャラクターに進みます(もちろん、.NET 4.0より前に作成されたレガシープロジェクトをサポートしていない場合)。

 public static TestResult TestDynamicCall(dynamic arg) { var instance = Container.Get(arg.GetType()); dynamic hook = CreateDelegate(instance, instance.GetType().GetMethod("Process")); long summ = 0; for (long i = 0; i < ITERATION_COUNT; i++) { summ += hook(arg); } // return }
      
      







デリゲートを介した呼び出しと比較して行ったことすべてに、動的キーワードを追加しました。これにより、ランタイムは、操作中にDLR自体を介してデリゲート呼び出しを構築できました。 実際、彼らは型チェックチェックを投げました。 また、最適化されたデリゲートの呼び出しに比べて2倍に加速しました。





UPD:プロンプトmayorovpで呼び出すより効率的な方法を追加しました。 速度で最高の結果を示し、ダイナミックと比較してメモリの消費が少なくなります。



 delegate object Invoker(object target, params object[] args); static Invoker CreateExpression(MethodInfo method) { var targetArg = Expression.Parameter(typeof(object)); var argsArg = Expression.Parameter(typeof(object[])); Expression body = Expression.Call( method.IsStatic ? null : Expression.Convert(targetArg, method.DeclaringType), method, method.GetParameters().Select((p, i) => Expression.Convert(Expression.ArrayIndex(argsArg, Expression.Constant(i)), p.ParameterType))); if (body.Type == typeof(void)) body = Expression.Block(body, Expression.Constant(null)); else if (body.Type.IsValueType) body = Expression.Convert(body, typeof(object)); return Expression.Lambda<Invoker>(body, targetArg, argsArg).Compile(); }
      
      







テスト

 public static TestResult TestExpressionCall(object arg) { var instance = Container.Get(arg.GetType()); var hook = CreateExpression(instance.GetType().GetMethod("Process")); long summ = 0; for (long i = 0; i < ITERATION_COUNT; i++) { summ += (long)hook(instance, arg); } //return }
      
      







プロジェクトコード



結果に戻る



All Articles