[C#][.NET] メタプログラミング入門 – Add メソッドのパフォーマンスの比較

Metasequoia

※ 「[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

今度は、どの結果も大差ない。毎回メソッドを直接呼ぶよりは寧ろ速いことが分かる。

まとめ

方法にも因るが、動的生成自体はそこそこハイコストであることが分かった。

従って、実行の都度、動的生成を行うのではなく、一度生成したデリゲートはキャッシュしておくのが有効だと思われる。

一度生成すれば、リフレクション等を用いた動的なコードとは異なり、通常のデリゲートなので、実行自体に時間が掛かる訳ではない。

次回からは、別のケースでキャッシュを用いた場合について検証していく。