※ 「[C#][.NET] メタプログラミング入門 - Roslyn による Add メソッドの動的生成」の続き。
C# によるメタプログラミングでのパフォーマンスの比較
前回まで、C# によるメタプログラミングで Add メソッドを動的生成するプログラムを作成してきた。
今回は、それぞれの手法における実行速度を測ってみよう。
実行に掛かった時間の測定用のクラス
それぞれの実行に掛かった時間を測る為、次のようなクラスを用意することにした。
using System; using System.Diagnostics; using System.Linq.Expressions; public static class パフォーマンステスター { public static void テスト(Expression<Action> 処理式, int 回数, Action<string> output) { // 処理でなく処理式として受け取っているのは、文字列として出力する為 var 処理 = 処理式.Compile(); var 時間 = 計測(処理, 回数).TotalMilliseconds; // 回数分の処理に掛かったミリ秒数 // 一回当たり何秒掛かったかを出力 output(string.Format("{0,70}: {1,10:F}/{2} 秒", 処理式.Body.ToString(), 時間, 回数 * 1000)); } static TimeSpan 計測(Action 処理, int 回数) { var stopwatch = new Stopwatch(); // 時間計測用 stopwatch.Start(); 回数.回(処理); stopwatch.Stop(); return stopwatch.Elapsed; } static void 回(this int @this, Action 処理) { for (var カウンター = 0; カウンター < @this; カウンター++) 処理(); } }
それでは実際に測ってみよう。
デリゲートの動的生成のパフォーマンスのテスト
先ずは、デリゲートの動的生成に掛かる時間。
これまでの三種類のコードを呼び出し、それぞれがデリゲートを作成するまでに掛かる時間を測ってみる。
using Roslyn.Scripting.CSharp; using System; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; static class Program { // Reflection.Emit の DynamicMethod による Add メソッドの生成 static Func<int, int, int> AddByEmit() { // DynamicMethod var method = new DynamicMethod( name : "add", returnType : typeof(int), parameterTypes: new[] { typeof(int), typeof(int) } ); // 引数 x 生成用 var x = method.DefineParameter(position: 1, attributes: ParameterAttributes.In, parameterName: "x"); // 引数 y 生成用 var y = method.DefineParameter(position: 2, attributes: ParameterAttributes.In, parameterName: "y"); // ILGenerator var generator = method.GetILGenerator(); // 生成したい IL // IL_0000: ldarg.0 // IL_0001: ldarg.1 // IL_0002: add // IL_0003: ret // 「最初の引数をスタックにプッシュする」コードを生成 generator.Emit(opcode: OpCodes.Ldarg_0); // 「二つ目の引数をスタックにプッシュ」コードを生成 generator.Emit(opcode: OpCodes.Ldarg_1); // 「二つの値を加算する」コードを生成 generator.Emit(opcode: OpCodes.Add ); // 「リターンする」コードを生成 generator.Emit(opcode: OpCodes.Ret ); // 動的にデリゲートを生成 return (Func<int, int, int>)method.CreateDelegate(delegateType: typeof(Func<int, int, int>)); } // Expression (式) による Add メソッドの生成 static Func<int, int, int> AddByExpression() { // 生成したい式 // (int x, int y) => x + y var x = Expression.Parameter(type: typeof(int)); // 引数 x の式 var y = Expression.Parameter(type: typeof(int)); // 引数 y の式 var add = Expression.Add (left: x, right: y); // x + y の式 var lambda = Expression.Lambda (add, x, y ); // (x, y) => x + y の式 // ラムダ式をコンパイルしてデリゲートとして返す return (Func<int, int, int>)lambda.Compile(); } // Roslyn による Add メソッドの生成 static Func<int, int, int> AddByRoslyn() { var engine = new ScriptEngine(); // C# のスクリプトエンジン var session = engine.CreateSession(); session.ImportNamespace("System"); // System 名前空間のインポート return (Func<int, int, int>)session.Execute(code: "(Func<int, int, int>)((x, y) => x + y)"); } static void Main() { 生成のパフォーマンステスト(); } static void 生成のパフォーマンステスト() { Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示 const int 回数 = 1000; パフォーマンステスト(() => AddByEmit (), 回数); // Reflectin.Emit による生成 パフォーマンステスト(() => AddByExpression(), 回数); // 式木による生成 パフォーマンステスト(() => AddByRoslyn (), 回数); // Roslyn による生成 } static void パフォーマンステスト(Expression<Action> 処理式, int 回数) { パフォーマンステスター.テスト(処理式, 回数, Console.WriteLine); } }
実行してみよう。
【生成のパフォーマンステスト】 AddByEmit(): 7.43/1000000 秒 AddByExpression(): 77.82/1000000 秒 AddByRoslyn(): 3088.51/1000000 秒
速い順に並べてみよう。
順位 | 方法 | 時間 (マイクロ秒) |
---|---|---|
1 | Reflection.Emit を使って動的にメソッドを生成した場合 | 7.43 |
2 | 式木を使って動的にメソッドを生成した場合 | 77.82 |
3 | Roslyn を使って動的にメソッドを生成した場合 | 3088.51 |
手間が掛からない方法程時間が掛かっているのが分かる。Roslyn は特に時間が掛かる。3088.51/1000000 秒ということは、約 0.003 秒も掛かっていることになる。
生成済みデリゲートの実行のパフォーマンスのテスト
次は、それぞれの動的生成済みのデリゲートを実行する時間だ。
今度のコードでは、デリゲートを動的生成する迄の時間は測らず、生成後のデリゲートの実行時間を測る。
using Roslyn.Scripting.CSharp; using System; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; static class Program { // 普通の静的な Add メソッド static int Add(int x, int y) { return x + y; } // Reflection.Emit の DynamicMethod による Add メソッドの生成 static Func<int, int, int> AddByEmit() { // DynamicMethod var method = new DynamicMethod( name : "add", returnType : typeof(int), parameterTypes: new[] { typeof(int), typeof(int) } ); // 引数 x 生成用 var x = method.DefineParameter(position: 1, attributes: ParameterAttributes.In, parameterName: "x"); // 引数 y 生成用 var y = method.DefineParameter(position: 2, attributes: ParameterAttributes.In, parameterName: "y"); // ILGenerator var generator = method.GetILGenerator(); // 生成したい IL // IL_0000: ldarg.0 // IL_0001: ldarg.1 // IL_0002: add // IL_0003: ret // 「最初の引数をスタックにプッシュする」コードを生成 generator.Emit(opcode: OpCodes.Ldarg_0); // 「二つ目の引数をスタックにプッシュ」コードを生成 generator.Emit(opcode: OpCodes.Ldarg_1); // 「二つの値を加算する」コードを生成 generator.Emit(opcode: OpCodes.Add ); // 「リターンする」コードを生成 generator.Emit(opcode: OpCodes.Ret ); // 動的にデリゲートを生成 return (Func<int, int, int>)method.CreateDelegate(delegateType: typeof(Func<int, int, int>)); } // Expression (式) による Add メソッドの生成 static Func<int, int, int> AddByExpression() { var x = Expression.Parameter(type: typeof(int)); // 引数 x の式 var y = Expression.Parameter(type: typeof(int)); // 引数 y の式 var add = Expression.Add (left: x, right: y); // x + y の式 var lambda = Expression.Lambda (add, x, y ); // (x, y) => x + y の式 // ラムダ式をコンパイルしてデリゲートとして返す return (Func<int, int, int>)lambda.Compile(); } // Roslyn による Add メソッドの生成 static Func<int, int, int> AddByRoslyn() { var engine = new ScriptEngine(); var session = engine.CreateSession(); session.ImportNamespace("System"); // System 名前空間のインポート return (Func<int, int, int>)session.Execute(code: "(Func<int, int, int>)((x, y) => x + y)"); } static void Main() { 実行のパフォーマンステスト(); } static void 実行のパフォーマンステスト() { Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示 // それぞれのデリゲートを準備 Func<int, int, int> add = Add ; var addByEmit = AddByEmit (); // Reflectin.Emit による生成 var addByExpression = AddByExpression(); // 式木による生成 var addByRoslyn = AddByRoslyn (); // Roslyn による生成 const int 回数 = 1000000; パフォーマンステスト(() => Add (1, 2), 回数); // 静的な Add を直接呼ぶ パフォーマンステスト(() => add (1, 2), 回数); // 静的な Add をデリゲートに入れて呼ぶ パフォーマンステスト(() => addByEmit (1, 2), 回数); // Reflectin.Emit によって生成済みのデリゲートを呼ぶ パフォーマンステスト(() => addByExpression(1, 2), 回数); // 式木によって生成済みのデリゲートを呼ぶ パフォーマンステスト(() => addByRoslyn (1, 2), 回数); // Roslyn によって生成済みのデリゲートを呼ぶ } static void パフォーマンステスト(Expression<Action> 処理式, int 回数) { パフォーマンステスター.テスト(処理式, 回数, Console.WriteLine); } }
実行してみよう。
【実行のパフォーマンステスト】 Add(1, 2): 12.64/1000000000 秒 Invoke(value(Program+<>c__DisplayClass0).add, 1, 2): 6.72/1000000000 秒 Invoke(value(Program+<>c__DisplayClass0).addEmit, 1, 2): 6.68/1000000000 秒 Invoke(value(Program+<>c__DisplayClass0).addExpression, 1, 2): 4.45/1000000000 秒 Invoke(value(Program+<>c__DisplayClass0).addRoslyn, 1, 2): 6.55/1000000000 秒
速い順に並べてみよう。
順位 | 方法 | 時間 (ナノ秒) |
---|---|---|
1 | 式木によって生成済みのデリゲートを呼ぶ | 4.45 |
2 | Roslyn によって生成済みのデリゲートを呼ぶ | 6.55 |
3 | Reflectin.Emit によって生成済みのデリゲートを呼ぶ | 6.68 |
4 | 静的な Add をデリゲートに入れて呼ぶ | 6.72 |
5 | 静的な Add を直接呼ぶ | 12.64 |
今度は、どの結果も大差ない。毎回メソッドを直接呼ぶよりは寧ろ速いことが分かる。
まとめ
方法にも因るが、動的生成自体はそこそこハイコストであることが分かった。
従って、実行の都度、動的生成を行うのではなく、一度生成したデリゲートはキャッシュしておくのが有効だと思われる。
一度生成すれば、リフレクション等を用いた動的なコードとは異なり、通常のデリゲートなので、実行自体に時間が掛かる訳ではない。
次回からは、別のケースでキャッシュを用いた場合について検証していく。