急に動的コード生成に興味がわいたので
Expression Tree (式木) のお勉強をしてサンプル的なものを書いてみた.
動機としては,.NET からデータベースを扱う方法を色々調べてて,Dapper とかの O/R Mapper がクエリの結果を型の付いたオブジェクトにマッピングするときどうやってるんだろうなーと思ったこと.自分で整理して考えてみると,
- ライブラリはユーザーに「この SQL を実行して結果を この型 (
TTarget
) でくれよー」と言われる - SQL を実行すると結果が
IDataReader
で返ってきて,そこから結果の各列の CLR 型情報がType
型で,名前がstring
で,各列の値がobject
で得られる
というわけで,TTarget
のインスタンスを生成してから,これらの情報を使ってプロパティの値をセットして返せばいいんだろう.実際のライブラリのソースを読んだわけじゃないけど,きっとそんなかんじだと思う.そのときにリフレクションを使ってたら遅いから,プロパティをセットするコードを動的に生成して使いまわす.
そこまで考えたところで,動的に生成したコードがどのくらい速いのか自分で確かめてみたくなって書いたのが以下のコード.普通のプロパティアクセス,リフレクション,式木のそれぞれで実行時間を測って比較してみた.参考にしたのはここ.
using System; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; namespace ExpressionTreeTest { class Program { static void Main(string[] args) { var obj = new MyClass(); TimeSpan time; // access normally time = MeasureExecutionTime(() => { foreach (var i in Enumerable.Range(1, 1000000)) { obj.MyProperty = i; } }); Console.WriteLine("normal: {0}ms", time.TotalMilliseconds); // access by reflection time = MeasureExecutionTime(() => { foreach (var i in Enumerable.Range(1, 1000000)) { typeof(MyClass).GetProperty("MyProperty").SetValue(obj, i); } }); Console.WriteLine("reflection: {0}ms", time.TotalMilliseconds); // access by compiled expression tree (generic) var setMyProperty = CreatePropertySetter<MyClass>(typeof(int), "MyProperty"); time = MeasureExecutionTime(() => { foreach (var i in Enumerable.Range(1, 1000000)) { setMyProperty(obj, i); } }); Console.WriteLine("expression tree (generic): {0}ms", time.TotalMilliseconds); // access by compiled expression tree (non-generic) var setMyProperty2 = CreatePropertySetter(typeof(MyClass), typeof(int), "MyProperty"); time = MeasureExecutionTime(() => { foreach (var i in Enumerable.Range(1, 1000000)) { setMyProperty2(obj, i); } }); Console.WriteLine("expression tree (non-generic): {0}ms", time.TotalMilliseconds); } static Action<TTarget, object> CreatePropertySetter<TTarget>(Type type, string name) where TTarget : class { var target = Expression.Parameter(typeof(TTarget), "target"); var value = Expression.Parameter(typeof(object), "value"); // (target, value) => target.(name) = (type)value に相当 var lambda = Expression.Lambda<Action<TTarget, object>>( Expression.Assign( Expression.Property(target, name), Expression.Convert(value, type) ), target, value ); return lambda.Compile(); } static Action<object, object> CreatePropertySetter(Type targetType, Type propertyType, string propertyName) { var target = Expression.Parameter(typeof(object), "target"); var value = Expression.Parameter(typeof(object), "value"); var lambda = Expression.Lambda<Action<object, object>>( Expression.Assign( Expression.Property( Expression.Convert(target, targetType), propertyName ), Expression.Convert(value, propertyType) ), target, value ); return lambda.Compile(); } static TimeSpan MeasureExecutionTime(Action action) { var sw = Stopwatch.StartNew(); action(); sw.Stop(); return sw.Elapsed; } } class MyClass { public int MyProperty { get; set; } } }
Expression Tree,今までは見ててもちんぷんかんぷんだったけど,だいぶ理解できるようになった感じ.プロパティへの代入がリフレクションだとSetValue
だけど,式木だと普通に "代入" としてExpression.Assign(property, value)
で書けることにちょっとびっくりした.ほんとにC#レベルの構文木が表現できるんだなー.
で,以下が実行結果.
normal: 7.6048ms reflection: 385.6697ms expression tree (generic): 23.4434ms expression tree (non-generic): 24.0111ms
だいたいこのページに書いてある通りの結果になった.動的コード生成が通常の数倍程度遅くて,リフレクションは桁違いに遅い.
あと,プロパティ取得の対象になるクラスをジェネリックで扱ったほうがExpression.Convert
が少なくなって速いのかなーとか思って式木版を2種類試したんだけど,ほとんど変わらなかったのが意外 (non-generic のほうが速くなるときもある).
ここまで来ればクエリ結果からオブジェクトへのマッピングも普通に出来る気がするし,そのうちやってみたい.