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

※ 「[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 |
今度は、どの結果も大差ない。毎回メソッドを直接呼ぶよりは寧ろ速いことが分かる。
まとめ
方法にも因るが、動的生成自体はそこそこハイコストであることが分かった。
従って、実行の都度、動的生成を行うのではなく、一度生成したデリゲートはキャッシュしておくのが有効だと思われる。
一度生成すれば、リフレクション等を用いた動的なコードとは異なり、通常のデリゲートなので、実行自体に時間が掛かる訳ではない。
次回からは、別のケースでキャッシュを用いた場合について検証していく。
関連
.NET.NET, C#, Expression, Metaprogramming, Reflection.Emit, Roslyn, メタプログラミング, 式木
Posted by Fujiwo
関連記事
[C#][AI/ML] C# でニューラルネットワークをスクラッチで書いて機械学習の原理を理解しよう | de:code 2018
Microsoft の『de:code 2018 | 開発者をはじめとする IT ...
[Event] 「BuriKaigi2019」 (2019年1月26日)を開催しました
富山県黒部市宇奈月温泉で、毎年恒例の勉強会「BuriKaigi2019」を開催し ...
C# Tips: 継承
今まで新人向けのオブジェクト指向の研修で、「継承」というのは、 class Su ...
[C#][.NET] メタプログラミング入門 – Reflection.Emit による Add メソッドの動的生成
※ 「 メタプログラミング入門 - はじめに」の続き。 Reflection.E ...
[C#] Windows 及び Internet Explorer のバージョンを調べる
■ 概要 C# で Windows 及び Internet Explorer の ...
ディスカッション
コメント一覧
まだ、コメントがありません