読者です 読者をやめる 読者になる 読者になる

プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

メタプログラミング入門 - Add メソッドのパフォーマンスの比較

.NET C#
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

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

まとめ

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

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

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

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