ぼやきごと/2011-06-05/動的アクセスにおける式木(Expression Trees)のパフォーマンス のバックアップの現在との差分(No.1)


  • 追加された行はこの色です。
  • 削除された行はこの色です。
#blog2navi()
*動的アクセスにおける式木(Expression Trees)のパフォーマンス [#hc2afdfd]

.NET Framework言語でクラスのプロパティへのアクセスやメソッドの呼び出しを動的に行う場合、そのメンバを保持するクラスの @code{Type}; オブジェクトから @code{PropertyInfo}; オブジェクトや @code{MethodInfo}; オブジェクトを取得してアクセスする…というのが.NET Framework 1.0の頃から使える方法でした。~
C#言語での簡単な例を次に示します。

#code(csharp){{
using System;
using System.Reflection;

namespace ReflectionSample
{
    class TestClass
    {
        public int Value { get; set; }

        public void Execute()
        {
            Console.WriteLine("Value = {0}", Value);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var test = new TestClass { Value = 100 };

            // Value プロパティの PropertyInfo を取得
            PropertyInfo valueInfo =
                typeof(TestClass).GetProperty("Value");

            // Value プロパティに動的アクセスして値を加算
            int temp = (int)valueInfo.GetValue(test, null);
            temp += 50;
            valueInfo.SetValue(test, temp, null);

            // Execute メソッドの MethodInfo を取得
            MethodInfo executeInfo =
                typeof(TestClass).GetMethod("Execute");

            // Execute メソッドを動的呼び出し
            executeInfo.Invoke(test, null);
        }
    }
}
}}

この方法の欠点は、とにかく実行速度が遅いことです。~
最新の.NET Frameworkではそこそこ改善されていますが、それでも静的アクセスと比べると文字通り桁違いに遅いです。

もう少しマシな速度の出る動的アクセス手法は無いものか…。~
というわけで(動的アクセスのためというわけではないでしょうが) .NET Framework 3.5 から登場したのが''式木(Expression Trees)''です。

式木とは、簡単に言ってしまえば @code{x + y}; のようなコードをそっくりそのままデータとして保持してしまう仕組みです。~
.NET Framework 3.5 では単文のラムダ式相当のコード((例: @code{x => x + 5};))のみ保持できましたが、 .NET Framework 4.0 では条件分岐やループを始めとした構文木を保持できるようになっています。~
式木について詳しくは次のサイトを参照してください。

-[[式木(Expression Trees) -- ++C++;// 未確認飛行 C>http://ufcpp.net/study/csharp/sp3_expression.html]]

そんなわけでC#の簡単なサンプルを作ってみました。~
静的アクセス、静的デリゲート呼び出し、 @code{PropertyInfo}; 利用、 @code{MethodInfo}; 利用、式木利用の5タイプ(式木は2通りなので全6通り)についてプロパティ値の取得&設定を100万回繰り返し、所要時間を計測しています。

#code(csharp){{
#define VCS2010

using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Diagnostics;

namespace ExpressionSample
{
    class TestData
    {
        public int Value { get; set; }
    }

    class Program
    {
        private static int LoopCount = 1000000;
        private const int LoopCount = 1000000;

        /// <summary>
        /// 静的な処理を行う。
        /// </summary>
        /// <param name="data">対象データ。</param>
        private static void DoStatic(TestData data)
        {
            for (int i = 0; i < LoopCount; ++i)
            {
                var temp = data.Value;
                ++temp;
                data.Value = temp;
            }
        }

        /// <summary>
        /// デリゲート呼び出しによる処理を行う。
        /// </summary>
        /// <param name="getter">値取得デリゲート。</param>
        /// <param name="setter">値設定デリゲート。</param>
        private static void DoByDelegate(
            Func<int> getter,
            Action<int> setter)
        {
            for (int i = 0; i < LoopCount; ++i)
            {
                var temp = getter();
                ++temp;
                setter(temp);
            }
        }

        /// <summary>
        /// PropertyInfo による処理を行う。
        /// </summary>
        /// <param name="data">対象データ。</param>
        /// <param name="propInfo">PropertyInfo 。</param>
        private static void DoByPropertyInfo(
            TestData data,
            PropertyInfo propInfo)
        {
            for (int i = 0; i < LoopCount; ++i)
            {
                var temp = (int)propInfo.GetValue(data, null);
                ++temp;
                propInfo.SetValue(data, temp, null);
            }
        }

        /// <summary>
        /// MethodInfo による処理を行う。
        /// </summary>
        /// <param name="data">対象データ。</param>
        /// <param name="getterInfo">値取得用 MethodInfo 。</param>
        /// <param name="setterInfo">値設定用 MethodInfo 。</param>
        private static void DoByMethodInfo(
            TestData data,
            MethodInfo getterInfo,
            MethodInfo setterInfo)
        {
            for (int i = 0; i < LoopCount; ++i)
            {
                var temp = (int)getterInfo.Invoke(data, null);
                ++temp;
                setterInfo.Invoke(data, new object[] { temp });
            }
        }

        /// <summary>
        /// 処理を行い、所要時間をコンソール出力する。
        /// </summary>
        /// <param name="name">処理名称。</param>
        /// <param name="action">処理デリゲート。</param>
        private static void Check(string name, Action action)
        {
            var sw = new Stopwatch();
            sw.Start();
            action();
            sw.Stop();
            Console.WriteLine(
                "{0,-12} : {1,12:F6}",
                name,
                sw.ElapsedTicks * 1000 / (double)Stopwatch.Frequency);
        }

        /// <summary>
        /// メインエントリポイント。
        /// </summary>
        static void Main(string[] args)
        {
            TestData data = new TestData { Value = 0 };

            // 静的呼び出し
            {
                data.Value = 0;
                Check("Static", () => DoStatic(data));
            }

            // デリゲートによる静的呼び出し
            {
                Func<int> getter = (() => data.Value);
                Action<int> setter = (v => data.Value = v);

                data.Value = 0;
                Check("Delegate", () => DoByDelegate(getter, setter));
            }

            // PropertyInfo による動的呼び出し
            {
                var propInfo = typeof(TestData).GetProperty("Value");

                data.Value = 0;
                Check(
                    "PropertyInfo",
                    () => DoByPropertyInfo(data, propInfo));
            }

            // MethodInfo による動的呼び出し
            {
                // PropertyInfo から各 MethodInfo を取得
                var propInfo = typeof(TestData).GetProperty("Value");
                var getterInfo = propInfo.GetGetMethod();
                var setterInfo = propInfo.GetSetMethod();

                data.Value = 0;
                Check(
                    "MethodInfo",
                    () => DoByMethodInfo(data, getterInfo, setterInfo));
            }

            // 式木(Expression)による MethodInfo からの動的呼び出し
            // .NET Framework 3.5 以降で利用可能
            {
                // PropertyInfo から各 MethodInfo を取得
                var propInfo = typeof(TestData).GetProperty("Value");
                var getterInfo = propInfo.GetGetMethod();
                var setterInfo = propInfo.GetSetMethod();

                // data の式木パラメータ作成
                var dataExp = Expression.Parameter(typeof(TestData), "data");

                // 設定する値の式木パラメータ作成
                var valueExp = Expression.Parameter(typeof(int), "value");

                // 値取得の式木を作成
                var getterCallExp = Expression.Call(dataExp, getterInfo);
                var getterExp =
                    Expression.Lambda<Func<TestData, int>>(
                        getterCallExp,
                        dataExp);

                // 値設定の式木を作成
                var setterCallExp =
                    Expression.Call(dataExp, setterInfo, valueExp);
                var setterExp =
                    Expression.Lambda<Action<TestData, int>>(
                        setterCallExp,
                        dataExp,
                        valueExp);

                // 式木をコンパイルして各デリゲート作成
                var getterLambda = getterExp.Compile();
                Func<int> getter = (() => getterLambda(data));
                var setterLambda = setterExp.Compile();
                Action<int> setter = (v => setterLambda(data, v));

                data.Value = 0;
                Check("Expression", () => DoByDelegate(getter, setter));

                // ついでに式木のラムダ式を出力してみる
                Console.WriteLine("  [getter] " + getterExp);
                Console.WriteLine("  [setter] " + setterExp);
            }

#if VCS2010
            // 式木(Expression)による動的呼び出し
            // .NET Framework 4.0 以降で利用可能
            {
                // data の式木パラメータ作成
                var dataExp = Expression.Parameter(typeof(TestData), "data");

                // Value プロパティの式木作成
                var dataValueExp = Expression.Property(dataExp, "Value");

                // 設定する値の式木パラメータ作成
                var valueExp = Expression.Parameter(typeof(int), "value");

                // 値取得の式木を作成
                // これは .NET Framework 3.5 でも可能
                var getterExp =
                    Expression.Lambda<Func<TestData, int>>(
                        dataValueExp,
                        dataExp);

                // 値設定の式木を作成
                var setterExp =
                    Expression.Lambda<Action<TestData, int>>(
                        Expression.Assign(dataValueExp, valueExp),
                        dataExp,
                        valueExp);

                // 式木をコンパイルして各デリゲート作成
                var getterLambda = getterExp.Compile();
                Func<int> getter = (() => getterLambda(data));
                var setterLambda = setterExp.Compile();
                Action<int> setter = (v => setterLambda(data, v));

                data.Value = 0;
                Check("Expression", () => DoByDelegate(getter, setter));

                // ついでに式木のラムダ式を出力してみる
                Console.WriteLine("  [getter] " + getterExp);
                Console.WriteLine("  [setter] " + setterExp);
            }
#endif // VCS2010
        }
    }
}
}}

このコードをVC#2008(.NET Framework 3.5)、VC#2010(.NET Framework 4.0)でそれぞれ実行した結果の出力は次の通りです。~
数字の単位はミリ秒です。~
VC#2008ではコード先頭の @code{#define VCS2010}; をコメントアウトしています。

:VC#2008|
#code(pre){{
Static       :    10.366139
Delegate     :    19.686542
PropertyInfo :  7400.734642
MethodInfo   :  7104.199582
Expression   :    28.631852
  [getter] data => data.get_Value()
  [setter] (data, value) => data.set_Value(value)
}}
:VC#2010|
#code(pre){{
Static       :    10.093576
Delegate     :    19.875913
PropertyInfo :  1353.851307
MethodInfo   :  1279.606984
Expression   :    64.919906
  [getter] data => data.get_Value()
  [setter] (data, value) => data.set_Value(value)
Expression   :    64.972448
  [getter] data => data.Value
  [setter] (data, value) => (data.Value = value)
}}

ご覧の通り、 @code{PropertyInfo}; や @code{MethodInfo}; を用いる方法と比べて圧倒的に速いです。~
特にVC#2008ではデリゲート呼び出しによる静的アクセスに肉薄する勢いです。~
VC#2010だとちょっと遅くなっているのが気になりますが、それでも十分速いと言えるでしょう((もっともC#4.0以降では @code{dynamic}; を使う場面が多いでしょうけど。))。

なお、ここまで高速になるのは式木をコンパイルしデリゲート化したものを利用した場合です。~
式木のコンパイルにはそれなりの時間が掛かるため、デリゲートをキャッシュする等して同じ式木を何度もコンパイルしないようする工夫が必要です。

RIGHT:Category: &#x5b;[[C#>ぼやきごと/カテゴリ/C#]]&#x5d;&#x5b;[[プログラミング>ぼやきごと/カテゴリ/プログラミング]]&#x5d; - 2011-06-05 07:52:13
----
RIGHT:&blog2trackback();
#comment(above)
#blog2navi()