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

Metasequoia

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

C# によるメタプログラミングでのパフォーマンスの比較

前回は、C# によるメタプログラミングで Add メソッドを動的生成した場合の実行速度を測定した。

今回も同様に、メソッドを動的生成した場合の実行速度を測定しよう。今回は、メソッド呼び出しのパフォーマンスを検証する。

次の順番で進めていく。

  • それぞれのメソッド呼び出し方法の紹介
    • 通常の静的なメソッド呼び出し
    • リフレクションを使った動的なメソッド呼び出し
    • dynamic を使った動的なメソッド呼び出し
    • Reflection.Emit を使って動的にメソッドを生成した場合
    • 式木を使って動的にメソッドを生成した場合
    • Roslyn を使って動的にメソッドを生成した場合
  • 実行に掛かった時間の測定用のクラスの準備
  • デリゲートの動的生成のパフォーマンスのテスト
    • Reflectin.Emit による生成
    • 式木による生成
    • Roslyn による生成
  • 実行のパフォーマンステスト
    • デリゲートの動的生成を行わない場合
      • デリゲートでなくメソッドを直接呼ぶ場合
      • メソッドをデリゲートに入れてから呼ぶ場合
      • リフレクションによる動的呼び出し
      • dynamic による動的呼び出し
    • 生成済みのデリゲートを使って呼ぶ場合
      • Reflectin.Emit による生成
      • 式木による生成
      • Roslyn による生成
  • キャッシュによる実行のパフォーマンステスト
    • コード生成しない場合と、コード生成しつつ呼ぶ場合 コード生成にキャッシュを効かせた場合

それぞれのメソッド呼び出し方法

先ずは、それぞれの実際の呼び出しの例を示そう。

  • 通常の静的なメソッド呼び出し
  • リフレクションを使った動的なメソッド呼び出し
  • dynamic を使った動的なメソッド呼び出し
  • Reflection.Emit を使って動的にメソッドを生成した場合
  • 式木を使って動的にメソッドを生成した場合
  • Roslyn を使って動的にメソッドを生成した場合

先ず、試しに呼び出すメソッドを準備しよう。

// 検証用のクラス
public class Something
{
// 検証用のメソッド
public string DoSomething()
{
return "Hello!";
}
}

この Something クラスのインスタンスの DoSomething() メソッドを呼び出すこととする。

順次、それぞれの方法での呼び出しを書いて行こう。

普通の静的なメソッド呼び出し
// 普通の静的なメソッド呼び出し
static string Call(Something item)
{
// 静的なメソッド呼び出し
return item.DoSomething();
}
リフレクションを使ったメソッド呼び出し

詳しくは以前の記事等を参考にしてほしい。

// リフレクションを使ったメソッド呼び出し
static object CallByReflection(object item)
{
// リフレクションを使って動的にメソッド情報を取得
var doSomething = item.GetType().GetMethod("DoSomething");
// メソッドの動的呼び出し
return doSomething.Invoke(item, null);
}
dynamic を使ったメソッド呼び出し

詳しくは以前の記事等を参考にしてほしい。

// dynamic を使ったメソッド呼び出し
static dynamic CallByDynamic(dynamic item)
{
// dynamic による動的なメソッド呼び出し
return item.DoSomething();
}
Reflection.Emit を使って動的にメソッドを生成した場合

[C#][.NET] メタプログラミング入門 – Reflection.Emit による Add メソッドの動的生成」でやったようにやってみよう。

生成したいコードを先ず C# で書いてビルドし、ILSpyIL (Intermediate Language) を調べてみよう。

次のような C# のソースコードをビルドし、

// 検証用のクラス
public class Something
{
// 検証用のメソッド
public string DoSomething()
{
return "Hello!";
}
}
static class Program
{
// 普通の静的なメソッド呼び出し
static string Call(Something item)
{
return item.DoSomething();
}
static void Main()
{}
}

Call メソッドの部分を ILSpy で見る。

ILSpy で Call メソッドの IL を見る
ILSpy で Call メソッドの IL を見る

参考にして、Reflection.Emit でメソッド生成を行ってみよう。

// using namespace 省略
// Reflection.Emit の DynamicMethod によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByEmit<T, TResult>(string methodName)
{
// DynamicMethod
var method = new DynamicMethod(
name          : "call"             ,
returnType    : typeof(string)     ,
parameterTypes: new[] { typeof(T) }
);
// 引数 item 生成用
var item = method.DefineParameter(position: 1, attributes: ParameterAttributes.In, parameterName: "item");
// ILGenerator
var generator = method.GetILGenerator();
// 生成したい IL
// IL_0000: ldarg.0
// IL_0001: callvirt instance void Something::DoSomething()
// IL_0006: ret
// 「引数をスタックにプッシュする」コードを生成
generator.Emit(opcode: OpCodes.Ldarg_0);
// 「指定された名前のメソッドを呼ぶ」コードを生成
generator.Emit(opcode: OpCodes.Callvirt, meth: typeof(T).GetMethod(name: methodName, types: Type.EmptyTypes));
// 「リターンする」コードを生成
generator.Emit(opcode: OpCodes.Ret);
// 動的にデリゲートを生成
return (Func<T, TResult>)method.CreateDelegate(delegateType: typeof(Func<T, TResult>));
}
式木を使って動的にメソッドを生成した場合

[C#][.NET][式木] メタプログラミング入門 – 式木による Add メソッドの動的生成」でやったようにやってみよう。

var item = new Something();
Expression<Func<Something, string>> call = item => item.DoSomething();

のようなコードを実行して、Visual Studio のデバッガーの「クイックウォッチ」等で call の構造を調べてみる。

call 式の構造
call 式の構造

これを参考にして、式木によるメソッド生成を行ってみよう。

// using namespace 省略
// Expression (式) によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByExpression<T, TResult>(string methodName)
{
// 生成したい式の例:
// (T item) => item.methodName()
// 引数 item の式
var parameterExpression = Expression.Parameter(type: typeof(T), name: "item");
// item.methodName() の式
var callExpression      = Expression.Call(
instance: parameterExpression,
method  : typeof(T).GetMethod(methodName, Type.EmptyTypes)
);
// item => item.methodName() の式
var lambda              = Expression.Lambda(callExpression, parameterExpression);
// ラムダ式をコンパイルしてデリゲートとして返す
return (Func<T, TResult>)lambda.Compile();
}
Roslyn を使って動的にメソッドを生成した場合

[C#][.NET][Roslyn] メタプログラミング入門 – Roslyn による Add メソッドの動的生成」でやったようにやってみよう。

// using namespace 省略
// Roslyn によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByRoslyn<T, TResult>(string methodName)
{
var engine  = new ScriptEngine(); // C# のスクリプトエンジン
engine.AddReference(typeof(T).Assembly); // 型 T のメソッドを参照するのに必要
engine.ImportNamespace("System");        // System 名前空間のインポート
var session = engine.CreateSession();
// 作りたいソースコードの例
// (Func<Something, string>)(item => item.DoSomething())
// ソースコードを文字列として作成
var code    = string.Format("(Func<{0}, {1}>)(item => item.{2}())", typeof(T).FullName, typeof(TResult).FullName, methodName);
// ソースコードをコンパイルしてデリゲートを生成して返す
return (Func<T, TResult>)session.Execute(code: code);
}
実行のテスト

まとめると次のようになる。

using Roslyn.Scripting.CSharp;
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
// 検証用のクラス
public class Something
{
// 検証用のメソッド
public string DoSomething()
{
return "Hello!";
}
}
static class Program
{
// 普通の静的なメソッド呼び出し
static string Call(Something item)
{
// 静的なメソッド呼び出し
return item.DoSomething();
}
// リフレクションを使ったメソッド呼び出し
static object CallByReflection(object item)
{
// リフレクションを使って動的にメソッド情報を取得
var doSomething = item.GetType().GetMethod("DoSomething");
// メソッドの動的呼び出し
return doSomething.Invoke(item, null);
}
// dynamic を使ったメソッド呼び出し
static dynamic CallByDynamic(dynamic item)
{
// dynamic による動的なメソッド呼び出し
return item.DoSomething();
}
// Reflection.Emit の DynamicMethod によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByEmit<T, TResult>(string methodName)
{
// DynamicMethod
var method = new DynamicMethod(
name          : "call"             ,
returnType    : typeof(string)     ,
parameterTypes: new[] { typeof(T) }
);
// 引数 item 生成用
var item = method.DefineParameter(position: 1, attributes: ParameterAttributes.In, parameterName: "item");
// ILGenerator
var generator = method.GetILGenerator();
// 生成したい IL
// IL_0000: ldarg.0
// IL_0001: callvirt instance void Something::DoSomething()
// IL_0006: ret
// 「引数をスタックにプッシュする」コードを生成
generator.Emit(opcode: OpCodes.Ldarg_0);
// 「指定された名前のメソッドを呼ぶ」コードを生成
generator.Emit(opcode: OpCodes.Callvirt, meth: typeof(T).GetMethod(name: methodName, types: Type.EmptyTypes));
// 「リターンする」コードを生成
generator.Emit(opcode: OpCodes.Ret);
// 動的にデリゲートを生成
return (Func<T, TResult>)method.CreateDelegate(delegateType: typeof(Func<T, TResult>));
}
// Expression (式) によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByExpression<T, TResult>(string methodName)
{
// 生成したい式の例:
// (T item) => item.methodName()
// 引数 item の式
var parameterExpression = Expression.Parameter(type: typeof(T), name: "item");
// item.methodName() の式
var callExpression      = Expression.Call(
instance: parameterExpression,
method  : typeof(T).GetMethod(methodName, Type.EmptyTypes)
);
// item => item.methodName() の式
var lambda              = Expression.Lambda(callExpression, parameterExpression);
// ラムダ式をコンパイルしてデリゲートとして返す
return (Func<T, TResult>)lambda.Compile();
}
// Roslyn によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByRoslyn<T, TResult>(string methodName)
{
var engine  = new ScriptEngine(); // C# のスクリプトエンジン
engine.AddReference(typeof(T).Assembly); // 型 T のメソッドを参照するのに必要
engine.ImportNamespace("System");        // System 名前空間のインポート
var session = engine.CreateSession();
// 作りたいソースコードの例
// (Func<Something, string>)(item => item.DoSomething())
// ソースコードを文字列として作成
var code    = string.Format("(Func<{0}, {1}>)(item => item.{2}())", typeof(T).FullName, typeof(TResult).FullName, methodName);
// ソースコードをコンパイルしてデリゲートを生成して返す
return (Func<T, TResult>)session.Execute(code: code);
}
static void Main()
{
実行のテスト();
}
static void 実行のテスト()
{
Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示
// テスト用のアイテム
var item               = new Something();
// 普通の静的なメソッド呼び出し
var answer             = Call            (item);
Console.WriteLine("answer            : {0}", answer            );
// リフレクションによる動的なメソッド呼び出し
var answerByReflection = CallByReflection(item);
Console.WriteLine("answerByReflection: {0}", answerByReflection);
// dynamic による動的なメソッド呼び出し
var answerByDynamic    = CallByDynamic   (item);
Console.WriteLine("answerByDynamic   : {0}", answerByDynamic   );
// Reflection.Emit の DynamicMethod によるメソッド呼び出しメソッドの生成
var callByEmit         = CallByEmit      <Something, string>("DoSomething");
var answerByEmit       = callByEmit      (item); // 生成したメソッドの呼び出し
Console.WriteLine("answerByEmit      : {0}", answerByEmit      );
// Expression (式) によるメソッド呼び出しメソッドの生成
var callByExpression   = CallByExpression<Something, string>("DoSomething");
var answerByExpression = callByExpression(item); // 生成したメソッドの呼び出し
Console.WriteLine("answerByExpression: {0}", answerByExpression);
// Roslyn によるメソッド呼び出しメソッドの生成
var callByRoslyn       = CallByRoslyn    <Something, string>("DoSomething");
var answerByRoslyn     = callByRoslyn    (item); // 生成したメソッドの呼び出し
Console.WriteLine("answerByRoslyn    : {0}", answerByRoslyn    );
}
}

実行してみると、次のようになる。

【実行のテスト】
answer            : Hello!
answerByReflection: Hello!
answerByDynamic   : Hello!
answerByEmit      : Hello!
answerByExpression: Hello!
answerByRoslyn    : Hello!

いずれの場合も、ちゃんとテスト用のアイテム item のメソッド DoSomething() を呼び出せていることが分かる。

実行に掛かった時間の測定用のクラス

実行に掛かった時間を測る為、今回も、次のクラスを用意する。

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,8: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;
// 検証用のクラス
public class Something
{
// 検証用のメソッド
public string DoSomething()
{
return "Hello!";
}
}
static class Program
{
// Reflection.Emit の DynamicMethod によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByEmit<T, TResult>(string methodName)
{
// DynamicMethod
var method = new DynamicMethod(
name          : "call"             ,
returnType    : typeof(string)     ,
parameterTypes: new[] { typeof(T) }
);
// 引数 item 生成用
var item = method.DefineParameter(position: 1, attributes: ParameterAttributes.In, parameterName: "item");
// ILGenerator
var generator = method.GetILGenerator();
// 生成したい IL
// IL_0000: ldarg.0
// IL_0001: callvirt instance void Something::DoSomething()
// IL_0006: ret
// 「引数をスタックにプッシュする」コードを生成
generator.Emit(opcode: OpCodes.Ldarg_0);
// 「指定された名前のメソッドを呼ぶ」コードを生成
generator.Emit(opcode: OpCodes.Callvirt, meth: typeof(T).GetMethod(name: methodName, types: Type.EmptyTypes));
// 「リターンする」コードを生成
generator.Emit(opcode: OpCodes.Ret);
// 動的にデリゲートを生成
return (Func<T, TResult>)method.CreateDelegate(delegateType: typeof(Func<T, TResult>));
}
// Expression (式) によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByExpression<T, TResult>(string methodName)
{
// 引数 item の式
var parameterExpression = Expression.Parameter(type: typeof(T), name: "item");
// item.methodName() の式
var callExpression      = Expression.Call(
instance: parameterExpression,
method  : typeof(T).GetMethod(methodName, Type.EmptyTypes)
);
// item => item.methodName() の式
var lambda              = Expression.Lambda(callExpression, parameterExpression);
// ラムダ式をコンパイルしてデリゲートとして返す
return (Func<T, TResult>)lambda.Compile();
}
// Roslyn によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByRoslyn<T, TResult>(string methodName)
{
var engine  = new ScriptEngine();
engine.AddReference(typeof(T).Assembly); // 型 T のメソッドを参照するのに必要
engine.ImportNamespace("System");        // System 名前空間のインポート
var session = engine.CreateSession();
// ソースコードを文字列として作成
var code    = string.Format("(Func<{0}, {1}>)(item => item.{2}())", typeof(T).FullName, typeof(TResult).FullName, methodName);
// ソースコードをコンパイルしてデリゲートを生成して返す
return (Func<T, TResult>)session.Execute(code: code);
}
static void Main()
{
生成のパフォーマンステスト();
}
static void 生成のパフォーマンステスト()
{
Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示
const int 回数 = 1000;
// Reflectin.Emit による生成
パフォーマンステスト(() => CallByEmit      <Something, string>("DoSomething"), 回数);
// 式木による生成
パフォーマンステスト(() => CallByExpression<Something, string>("DoSomething"), 回数);
// Roslyn による生成
パフォーマンステスト(() => CallByRoslyn    <Something, string>("DoSomething"), 回数);
}
static void パフォーマンステスト(Expression<Action> 処理式, int 回数)
{
パフォーマンステスター.テスト(処理式, 回数, Console.WriteLine);
}
}

実行してみよう。

【生成のパフォーマンステスト】
CallByEmit("DoSomething"):     5.66/1000000 秒
CallByExpression("DoSomething"):   106.06/1000000 秒
CallByRoslyn("DoSomething"):  3474.70/1000000 秒

速い順に並べてみよう。

順位 方法 時間 (マイクロ秒)
1 Reflection.Emit を使って動的にメソッドを生成した場合 5.66
2 式木を使って動的にメソッドを生成した場合 106.06
3 Roslyn を使って動的にメソッドを生成した場合 3474.70

前回同様、手間が掛からない方法程生成に時間が掛かっている。

生成済みデリゲートの実行のパフォーマンスのテスト

次は、それぞれの方法によるデリゲートを実行する時間を測ろう。

デリゲートを動的生成場合は、動的生成する迄の時間は測らず、生成後のデリゲートの実行時間を測る。

using Roslyn.Scripting.CSharp;
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
// 検証用のクラス
public class Something
{
// 検証用のメソッド
public string DoSomething()
{
return "Hello!";
}
}
static class Program
{
// 普通の静的なメソッド呼び出し
static string Call(Something item)
{
// 静的なメソッド呼び出し
return item.DoSomething();
}
// リフレクションを使ったメソッド呼び出し
static object CallByReflection(object item)
{
// リフレクションを使って動的にメソッド情報を取得
var doSomething = item.GetType().GetMethod("DoSomething");
// メソッドの動的呼び出し
return doSomething.Invoke(item, null);
}
// dynamic を使ったメソッド呼び出し
static dynamic CallByDynamic(dynamic item)
{
// dynamic による動的なメソッド呼び出し
return item.DoSomething();
}
// Reflection.Emit の DynamicMethod によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByEmit<T, TResult>(string methodName)
{
// DynamicMethod
var method = new DynamicMethod(
name          : "call"             ,
returnType    : typeof(string)     ,
parameterTypes: new[] { typeof(T) }
);
// 引数 item 生成用
var item = method.DefineParameter(position: 1, attributes: ParameterAttributes.In, parameterName: "item");
// ILGenerator
var generator = method.GetILGenerator();
// 生成したい IL
// IL_0000: ldarg.0
// IL_0001: callvirt instance void Something::DoSomething()
// IL_0006: ret
// 「引数をスタックにプッシュする」コードを生成
generator.Emit(opcode: OpCodes.Ldarg_0);
// 「指定された名前のメソッドを呼ぶ」コードを生成
generator.Emit(opcode: OpCodes.Callvirt, meth: typeof(T).GetMethod(name: methodName, types: Type.EmptyTypes));
// 「リターンする」コードを生成
generator.Emit(opcode: OpCodes.Ret);
// 動的にデリゲートを生成
return (Func<T, TResult>)method.CreateDelegate(delegateType: typeof(Func<T, TResult>));
}
// Expression (式) によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByExpression<T, TResult>(string methodName)
{
// 引数 item の式
var parameterExpression = Expression.Parameter(type: typeof(T), name: "item");
// item.methodName() の式
var callExpression      = Expression.Call(
instance: parameterExpression,
method  : typeof(T).GetMethod(methodName, Type.EmptyTypes)
);
// item => item.methodName() の式
var lambda              = Expression.Lambda(callExpression, parameterExpression);
// ラムダ式をコンパイルしてデリゲートとして返す
return (Func<T, TResult>)lambda.Compile();
}
// Roslyn によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByRoslyn<T, TResult>(string methodName)
{
var engine  = new ScriptEngine();
engine.AddReference(typeof(T).Assembly); // 型 T のメソッドを参照するのに必要
engine.ImportNamespace("System");        // System 名前空間のインポート
var session = engine.CreateSession();
// ソースコードを文字列として作成
var code    = string.Format("(Func<{0}, {1}>)(item => item.{2}())", typeof(T).FullName, typeof(TResult).FullName, methodName);
// ソースコードをコンパイルしてデリゲートを生成して返す
return (Func<T, TResult>)session.Execute(code: code);
}
static void Main()
{
実行のパフォーマンステスト();
}
static void 実行のパフォーマンステスト()
{
Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示
// 検証用のアイテム
var                      item             = new Something();
// それぞれのデリゲートを準備
Func<Something, string > call             = Call                                              ;
Func<object   , object > callByReflection = CallByReflection                                  ;
Func<dynamic  , dynamic> callByDynamic    = CallByDynamic                                     ;
var                      callByEmit       = CallByEmit      <Something, string>("DoSomething");
var                      callByExpression = CallByExpression<Something, string>("DoSomething");
var                      callByRoslyn     = CallByRoslyn    <Something, string>("DoSomething");
const int                回数             = 1000000;
// デリゲートの動的生成を行わない場合
パフォーマンステスト(() => Call            (item), 回数); // 静的メソッドを直接呼ぶ場合
パフォーマンステスト(() => call            (item), 回数); // 静的メソッドをデリゲートに入れてから呼ぶ場合
パフォーマンステスト(() => callByReflection(item), 回数); // リフレクションによる動的呼び出し
パフォーマンステスト(() => callByDynamic   (item), 回数); // dynamic による動的呼び出し
// 生成済みのデリゲートを使って呼ぶ場合
パフォーマンステスト(() => callByEmit      (item), 回数); // Reflectin.Emit による生成
パフォーマンステスト(() => callByExpression(item), 回数); // 式木による生成
パフォーマンステスト(() => callByRoslyn    (item), 回数); // Roslyn による生成
}
static void パフォーマンステスト(Expression<Action> 処理式, int 回数)
{
パフォーマンステスター.テスト(処理式, 回数, Console.WriteLine);
}
}

実行してみよう。

【実行のパフォーマンステスト】
Call(value(Program+<>c__DisplayClass2).item):    16.03/1000000000 秒
Invoke(value(Program+<>c__DisplayClass2).call, value(Program+<>c__DisplayClass2).item):    10.74/1000000000 秒
Invoke(value(Program+<>c__DisplayClass2).callByReflection, value(Program+<>c__DisplayClass2).item):   316.62/1000000000 秒
Invoke(value(Program+<>c__DisplayClass2).callByDynamic, value(Program+<>c__DisplayClass2).item):    57.93/1000000000 秒
Invoke(value(Program+<>c__DisplayClass2).callByEmit, value(Program+<>c__DisplayClass2).item):    20.66/1000000000 秒
Invoke(value(Program+<>c__DisplayClass2).callByExpression, value(Program+<>c__DisplayClass2).item):    18.06/1000000000 秒
Invoke(value(Program+<>c__DisplayClass2).callByRoslyn, value(Program+<>c__DisplayClass2).item):     8.01/1000000000 秒

速い順に並べてみよう。

順位 方法 時間 (ナノ秒)
1 Roslyn を使って動的にメソッドを生成した場合 8.01
2 静的メソッドをデリゲートに入れてから呼ぶ場合 10.74
3 静的メソッドを直接呼ぶ場合 16.03
4 式木を使って動的にメソッドを生成した場合 18.06
5 Reflection.Emit を使って動的にメソッドを生成した場合 20.66
6 dynamic を使った動的なメソッド呼び出し 57.93
7 リフレクションを使った動的なメソッド呼び出し 316.62

動的生成が終わってしまえば、その後のデリゲートの実行は通常のデリゲートと変わらない。

動的なメソッド呼び出しは呼び出す度に遅い (特にリフレクション) ので、場合によっては動的生成にパフォーマンス上のメリットがある。

キャッシュによる実行のパフォーマンステスト

つまり、動的にメソッドを生成する場合でも、生成済みのメソッドをうまくキャッシュしてやれば、パフォーマンスが大きく向上することになる。

キャッシュを使った場合と使わないで実行の度にメソッドを生成した場合の速度を測ってみよう。

先ずは、生成済みデリゲートをキャッシュして呼び出すクラスを作ろう。

デリゲートのキャッシュ クラス
using System;
using System.Collections.Generic;
using System.Text;
// デリゲートのキャッシュ
public static class DelegateCache
{
// 生成したデリゲートのキャッシュ
static readonly Dictionary<string, Delegate> methods = new Dictionary<string, Delegate>();
// メソッド呼び出し
public static TResult Call<T, TResult>(this T @this, string methodName, Func<string, Func<T, TResult>> generator)
{
var targetType = @this.GetType();
var key        = ToKey(targetType, methodName);
ToCache<T, TResult>(key: key, item: @this, methodName: methodName, generator: generator); // キャッシュする
// キャッシュ内のデリゲートを呼ぶ
return ((Func<T, TResult>)methods[key: key])(@this);
}
// キャッシュに入れる
static void ToCache<T, TResult>(string key, T item, string methodName, Func<string, Func<T, TResult>> generator)
{
if (!methods.ContainsKey(key: key)) {     // キャッシュに無い場合は
var method = generator(methodName);   // 動的にデリゲートを生成して
methods.Add(key: key, value: method); // キャッシュに格納
}
}
// キーに変換
static string ToKey(Type type, string methodName)
{
return new StringBuilder().Append(type.FullName).Append(".").Append(methodName).ToString();
}
}

それでは、実際に測ってみよう。

using Roslyn.Scripting.CSharp;
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
// 検証用のクラス
public class Something
{
// 検証用のメソッド
public string DoSomething()
{
return "Hello!";
}
}
static class Program
{
// 普通の静的なメソッド呼び出し
static string Call(Something item)
{
// 静的なメソッド呼び出し
return item.DoSomething();
}
// リフレクションを使ったメソッド呼び出し
static object CallByReflection(object item)
{
// リフレクションを使って動的にメソッド情報を取得
var doSomething = item.GetType().GetMethod("DoSomething");
// メソッドの動的呼び出し
return doSomething.Invoke(item, null);
}
// dynamic を使ったメソッド呼び出し
static dynamic CallByDynamic(dynamic item)
{
// dynamic による動的なメソッド呼び出し
return item.DoSomething();
}
// Reflection.Emit の DynamicMethod によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByEmit<T, TResult>(string methodName)
{
// DynamicMethod
var method = new DynamicMethod(
name          : "call"             ,
returnType    : typeof(string)     ,
parameterTypes: new[] { typeof(T) }
);
// 引数 item 生成用
var item = method.DefineParameter(position: 1, attributes: ParameterAttributes.In, parameterName: "item");
// ILGenerator
var generator = method.GetILGenerator();
// 生成したい IL
// IL_0000: ldarg.0
// IL_0001: callvirt instance void Something::DoSomething()
// IL_0006: ret
// 「引数をスタックにプッシュする」コードを生成
generator.Emit(opcode: OpCodes.Ldarg_0);
// 「指定された名前のメソッドを呼ぶ」コードを生成
generator.Emit(opcode: OpCodes.Callvirt, meth: typeof(T).GetMethod(name: methodName, types: Type.EmptyTypes));
// 「リターンする」コードを生成
generator.Emit(opcode: OpCodes.Ret);
// 動的にデリゲートを生成
return (Func<T, TResult>)method.CreateDelegate(delegateType: typeof(Func<T, TResult>));
}
// Expression (式) によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByExpression<T, TResult>(string methodName)
{
// 引数 item の式
var parameterExpression = Expression.Parameter(type: typeof(T), name: "item");
// item.methodName() の式
var callExpression      = Expression.Call(
instance: parameterExpression,
method  : typeof(T).GetMethod(methodName, Type.EmptyTypes)
);
// item => item.methodName() の式
var lambda              = Expression.Lambda(callExpression, parameterExpression);
// ラムダ式をコンパイルしてデリゲートとして返す
return (Func<T, TResult>)lambda.Compile();
}
// Roslyn によるメソッド呼び出しメソッドの生成
static Func<T, TResult> CallByRoslyn<T, TResult>(string methodName)
{
var engine  = new ScriptEngine();
engine.AddReference(typeof(T).Assembly); // 型 T のメソッドを参照するのに必要
engine.ImportNamespace("System");        // System 名前空間のインポート
var session = engine.CreateSession();
// ソースコードを文字列として作成
var code    = string.Format("(Func<{0}, {1}>)(item => item.{2}())", typeof(T).FullName, typeof(TResult).FullName, methodName);
// ソースコードをコンパイルしてデリゲートを生成して返す
return (Func<T, TResult>)session.Execute(code: code);
}
static void Main()
{
キャッシュによる実行のパフォーマンステスト();
}
static void キャッシュによる実行のパフォーマンステスト()
{
Console.WriteLine("【{0}】", MethodBase.GetCurrentMethod().Name); // メソッド名を表示
// 検証用のアイテム
var       item = new Something();
const int 回数 = 1000;
// コード生成しない場合
パフォーマンステスト(() => Call            (item), 回数); // 静的メソッド
パフォーマンステスト(() => CallByReflection(item), 回数); // リフレクションによる動的呼び出し
パフォーマンステスト(() => CallByDynamic   (item), 回数); // dynamic による動的呼び出し
// コード生成しつつ呼ぶ場合 (Reflection.Emit、式木、Roslyn の順)
パフォーマンステスト(() => CallByEmit      <Something, string>("DoSomething")(item), 回数);
パフォーマンステスト(() => CallByExpression<Something, string>("DoSomething")(item), 回数);
パフォーマンステスト(() => CallByRoslyn    <Something, string>("DoSomething")(item), 回数);
// コード生成にキャッシュを効かせた場合 (Reflection.Emit、式木、Roslyn の順)
パフォーマンステスト(() => item.Call<Something, string>("DoSomething", methodName => CallByEmit      <Something, string>(methodName)), 回数);
パフォーマンステスト(() => item.Call<Something, string>("DoSomething", methodName => CallByExpression<Something, string>(methodName)), 回数);
パフォーマンステスト(() => item.Call<Something, string>("DoSomething", methodName => CallByRoslyn    <Something, string>(methodName)), 回数);
}
static void パフォーマンステスト(Expression<Action> 処理式, int 回数)
{
パフォーマンステスター.テスト(処理式, 回数, Console.WriteLine);
}
}

実行結果は次の通り。

【キャッシュによる実行のパフォーマンステスト】
Call(value(Program+<>c__DisplayClass6).item):     0.02/1000000 秒
CallByReflection(value(Program+<>c__DisplayClass6).item):     0.41/1000000 秒
CallByDynamic(value(Program+<>c__DisplayClass6).item):     0.05/1000000 秒
Invoke(CallByEmit("DoSomething"), value(Program+<>c__DisplayClass6).item):    52.71/1000000 秒
Invoke(CallByExpression("DoSomething"), value(Program+<>c__DisplayClass6).item):    97.02/1000000 秒
Invoke(CallByRoslyn("DoSomething"), value(Program+<>c__DisplayClass6).item):  2349.83/1000000 秒
value(Program+<>c__DisplayClass6).item.Call("DoSomething", methodName => CallByEmit(methodName)):     6.88/1000000 秒
value(Program+<>c__DisplayClass6).item.Call("DoSomething", methodName => CallByExpression(methodName)):     3.36/1000000 秒
value(Program+<>c__DisplayClass6).item.Call("DoSomething", methodName => CallByRoslyn(methodName)):     3.49/1000000 秒

こちらも速い順に並べてみよう。

順位 方法 時間 (マイクロ秒)
1 静的メソッド 0.02
2 dynamic による動的呼び出し 0.05
3 リフレクションによる動的呼び出し 0.41
4 式木 (キャッシュ有り) 3.36
5 Roslyn (キャッシュ有り) 3.49
6 Reflection.Emit (キャッシュ有り) 6.88
7 Reflection.Emit (キャッシュ無し) 52.71
8 式木 (キャッシュ無し) 97.02
9 Roslyn (キャッシュ無し) 2349.83

キャッシュ テーブルのオーバーヘッドはあるが、動的生成を行う場合は、キャッシュがとても有効であることが分かる。

まとめ

今回は、とても簡単なメソッド呼び出しの例で、静的な例、動的な例、動的にコードを生成する三通りの例の比較を行った。また、動的にコードを生成する場合にはキャッシュが有効であることを示した。

次回からは、もう少し応用例をあげていきたい。