Home / ぼやきごと / 2011-06-05
2011-06-05

動的アクセスにおける式木(Expression Trees)のパフォーマンス

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

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 
 
 
 
-
|
-
|
|
|
-
|
!
!
|
|
-
|
-
|
|
-
!
|
|
-
!
|
|
|
-
!
|
|
-
!
!
!
!
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)です。

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

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

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
 
 
 
 
 
 
 
 
-
|
-
|
!
|
|
-
|
|
-
|
|
|
!
-
|
-
|
|
|
!
!
|
-
|
|
|
|
!
|
|
-
|
-
|
|
|
!
!
|
-
|
|
|
|
!
|
|
-
|
-
|
|
|
!
!
|
-
|
|
|
|
|
!
|
|
|
-
|
-
|
|
|
!
!
|
-
|
|
|
|
!
-
|
|
|
|
|
|
|
|
!
|
-
|
|
!
-
|
|
-
!
|
|
!
|
-
!
|
|
|
|
|
!
|
-
!
|
|
|
|
|
|
!
 
-
!
-
!
 
 
 
 
 
 
 
!
|
-
 
!
-
!
|
|
|
-
!
|
-
!
|
-
!
|
|
|
|
|
-
!
|
|
|
|
|
|
|
-
!
|
|
|
|
|
|
|
-
!
|
!
|
|
-
|
!
-
!
|
-
!
|
-
!
|
-
|
!
|
|
|
|
-
!
|
|
|
|
|
-
!
|
|
|
|
|
|
|
-
!
|
!
|
!
!
!
#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 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ではコード先頭の #define VCS2010 をコメントアウトしています。

VC#2008
  1
  2
  3
  4
  5
  6
  7
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
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
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)

ご覧の通り、 PropertyInfoMethodInfo を用いる方法と比べて圧倒的に速いです。
特にVC#2008ではデリゲート呼び出しによる静的アクセスに肉薄する勢いです。
VC#2010だとちょっと遅くなっているのが気になりますが、それでも十分速いと言えるでしょう*2

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

Category: [C#][プログラミング] - 2011-06-05 07:52:13

*1 例: x => x + 5
*2 もっともC#4.0以降では dynamic を使う場面が多いでしょうけど。