Expression Treeのこね方・入門編 - 動的にデリゲートを生成してリフレクションを高速化
- 2011-04-20
Expression Treeは、IQueryableの中心、Code as Dataなわけですが、それ以外にも用途は色々あります。ただたんに名前を取り出すだけ(考えてみると贅沢な使い方よね)とか、デリゲートを生成したりとか。varはLinqのために導入されたものだからそれ以外に無闇に使うのは良くない(キリッ とか言う人は、式木も同じ考えなんですかね、匿名型へも同じ態度で?導入された、そして発展させたのはLinqだとしても、別にそれ以外に使ってもいいんだよって。縛られた考えイクナイ。
というわけで、今更に、初歩からの式木再入門。特に.NET 4から大幅に拡張されて式だけじゃなく文までいけるようになって、何でも表現出来るようになりました。式木の用途は多岐に渡るわけですが、今回はリフレクションの高速化をお題にしたいと思います。プロパティ名の文字列からPropertyInfoを取ってGetValueといったように、動的に値のGet/Setをするわけですが、それを動的コード生成で高速化しよう!
方針としては、プロパティアクセスのデリゲートを生成します。GetだったらFunc<object, object>を作って、引数が対象インスタンス、戻り値が取得結果の値、といった具合です。まんまPropertyInfoのGetValueがデリゲートになったもの、といった具合。
では、実際に書きながら見ていきます。というわけで、私の書き方などりを順を追って。まず、何をやりたいかを明確にするため、実際のコードで、具体的なラムダ式を書いてコンパイル通します。
// 適当なクラス
class MyClass
{
public int MyProperty { get; set; }
}
static void Main(string[] args)
{
// これで書くと(.NET 4以降の)代入やループはサポートされてないので、Funcでも全然いいです
// ただ、デバッガで生成された式の結果が見えるので、Expressionでコンパイル通せたほうが楽かな
Expression<Func<object, object>> expr =
target => ((MyClass)target).MyProperty;
}
コンパイル通るということは、それで書けるということ。机上で、頭の中だけで考えてもいいことありません。ささっとコンパイラ使いませう。そして一旦デバッガでexprを覗いてみますと
// ToString()
// target => Convert(Convert(target).MyProperty)
// DebugView
.Lambda #Lambda1<System.Func`2[System.Object,System.Object]>(System.Object $target) {
(System.Object)((Program+MyClass)$target).MyProperty
}
Expressionで宣言して書くというのは、コンパイラがコンパイル時に式木を生成するということです。見た目はFuncに毛が生えた程度なのに、コンパイル結果は大違い。なんて恐ろしい!そして、なんて素晴らしい!ともあれ、結果から逆算してくのが手っ取り早い。大枠は機械生成に任せてしまって、微調整だけを手動でやればいいわけで。無から作るのは大変ですが、枠組みが出来ているなら簡単だもの。このToStringとDebugViewは大変便利です。なお、リフレクタで生成結果を見るという夢のない方法もあります。
上記結果から、具体的な成分に置き換えた、何を書くのかの式をイメージ。
// (object target) => (object)((T)target).PropertyName
もう、あとは機械的に置き換えていくだけ!というわけで、具体的にこねこねしていきますが、まずパラメータとラムダ本体を用意します。この二つは、最終的にデリゲートの生成を目指した式木の作成では定型句みたいなものなので何も考えず用意。
// まず、引数のパラメータとLambda本体を書く
var target = Expression.Parameter(typeof(object), "target");
var lambda = Expression.Lambda<Func<object, object>>(
/* body */
, target);
基本的に、埋めやすいところから埋めていくのがいいのではないかなー。そして、Expressionは最後の引数が一番埋めやすいので、外から内に向かって書いていくことになります。最初のLambdaは、引数パラメータを最初に置いてしまって、bodyは後回しにする。というわけで、これで左辺は終了しました。次は右辺。外周から構築されるので、まずobjectへのキャスト。これはExpression.Convertです。
var lambda = Expression.Lambda<Func<object, object>>(
Expression.Convert(
/* body */
, typeof(object))
, target);
当然ですが、ちゃんとインデントつけたほうがいいです。カンマ前置は気持ち悪いですが、こうして後ろから埋めていく時は、こっちのほうが書きやすいかなー。気持ち悪ければ最後にまとめて直せばいいのではないかと。
次はTへのキャスト、ではなくプロパティ呼び出し。実行の順番はTにキャスト→プロパティ呼び出し→objectにキャストですからね。プロパティ呼び出しはExpression.Property。ですが、フィールドも似たようなものだし、幸いExpressionには両者を区別しないPropertyOrFieldがあるので、そちらを使いましょう。名前はstringで渡しますが、とりあえず"PropertyName"で。
var lambda = Expression.Lambda<Func<object, object>>(
Expression.Convert(
Expression.PropertyOrField(
/* body */
, "PropertyName")
, typeof(object))
, target);
最後は((T)target)。(T)は後で置き換えるとして、とりあえずMyClassにしておきますか。targetは、最初に作った右辺のパラメータです。
var target = Expression.Parameter(typeof(object), "target");
var lambda = Expression.Lambda<Func<object, object>>(
Expression.Convert(
Expression.PropertyOrField(
Expression.Convert(
target
, typeof(MyClass))
, "PropertyName")
, typeof(object))
, target);
埋まった!埋まったらとりあえずまずコンパイル。するとPropertyNameはMyClassにないよ、と例外出て終了。ふむふむ。ところで、ここでチェック入るんですね、へー。それでは、別関数に分けることを意識して、(ようやく)変数用意しますか。
// あとで関数の引数にするとして
var type = typeof(MyClass);
var propertyName = "MyProperty";
var target = Expression.Parameter(typeof(object), "target");
var lambda = Expression.Lambda<Func<object, object>>(
Expression.Convert(
Expression.PropertyOrField(
Expression.Convert(
target
, type)
, propertyName)
, typeof(object))
, target);
コンパイル通ったー。そしたら、とりあえずデバッガでlambda変数を観察。ToString結果とDebugViewプロパティを見るといいでしょう。
// lambda.ToString()
target => Convert(Convert(target).MyProperty)
// lambda.DebugView
.Lambda #Lambda1<System.Func`2[System.Object,System.Object]>(System.Object $target) {
(System.Object)((Program+MyClass)$target).MyProperty
}
問題なさそうですね!この二つは作るときに非常に便利なので、大きめのを書くときは断片を書いてこれでチェック、みたいにするといいかも。では、最後にデリゲート生成(Compile)を。
// デリゲート生成!
var func = lambda.Compile();
// てすと
var test = new MyClass { MyProperty = 200 };
var result = func(test);
Console.WriteLine(result);
というわけでした。DynamicMethodでILもにゃもにゃ(敷居高すぎ!)とか、Delegate.CreateDelegateだのでもにゃもにゃ(面倒くさい!)に比べると、随分素直に書けて素敵。.NET 4.0からはブロックやループなど、式だけではなく全ての表現が可能になったので、動的コード生成が身近になりました。
Setのほうも同様な感じに書けます。
// (object target, object value) => ((T)target).memberName = (U)value
static Action<object, object> CreateSetDelegate(Type type, string memberName)
{
var target = Expression.Parameter(typeof(object), "target");
var value = Expression.Parameter(typeof(object), "value");
var left =
Expression.PropertyOrField(
Expression.Convert(target, type), memberName);
var right = Expression.Convert(value, left.Type);
var lambda = Expression.Lambda<Action<object, object>>(
Expression.Assign(left, right),
target, value);
return lambda.Compile();
}
// Test
static void Main(string[] args)
{
var target = new MyClass { MyProperty = 200 };
var accessor = CreateSetDelegate(typeof(MyClass), "MyProperty");
accessor(target, 1000); // set
Console.WriteLine(target.MyProperty); // 1000
}
Expression.Assignが代入なのと、objectで渡されるvalueは、プロパティに代入する際にプロパティの型へキャストする必要があるので、left.Typeで取り出しています。これ、Lambdaの中に一気に書いてしまうと値が取れないので、外で書く必要があるのが少々面倒かしらん。とりあえず、コメントで生成後の式を書いておいてあげると見る人に(少しだけ)優しい。
今回のSet/Getは微妙に汎用的なものにするため全てobjectで扱っていますが、ジェネリクスにすれば、余計なExpression.Convertがなくてスッキリ記述+パフォーマンスも向上、が狙えそうですねん。
何で突然?
思うところあって、じゃなくて、WP7にSQLCE搭載の報を受けて、以前書いたデータベース用ユーティリティを書き直そうと思いまして。そして、コマンドパラメータの受け渡しには匿名型を使おうかな、と。そうすると、書くのがとても簡単になるんですね。凄く軽快で。これは良い。のですけど、実行の度にPropertyInfoを取ってきてNameとGetValueでの値取り出しはどうかと思ったわけです。そりゃねーよ、と。そこで、じゃあ、キャッシュしよう。キャッシュするならPropertyInfoをキャッシュしたってそんな速くはない、やるならデリゲート生成までやろう。と、紆余曲折あってそうなりました。
パラメータだけではなくて、簡易マッパー(selectの各カラムの名前とプロパティ名からインスタンス生成)も用意しているのですが、それもデリゲートのキャッシュで高速化効いてくるかなー、と。
で、速いのかというと、うーん、生成のコストが結構高いので、平均取ると、PropertyInfoのキャッシュと比べると、数千回実行しないとコスト回収出来ないかも。PropertyInfoのGetValueも遅い遅いというほどにそんな遅くないのかなあ、いや、デリゲートと比べると十数倍ぐらいは違うんですが、しかし。マッパー的に使って、一回の実行に100行取ってくる、とかだったら余裕ですぐ回収出来ますが、コマンド程度だとどうだろうなー。まあ、ASP.NET MVCなんかはTypeDescriptor(正直遅い)経由でやってるみたいだし、それと比べれば悪くないかもはしれない。でもWP7を見ると、アプリケーションのキャッシュ生存期間を考えると、ペイ出来そうな気がしない。
とはいえ、初回に少し重くてあとは高速、のほうがユーザーエクスペリエンス的にはいいかな、と思うので(あと、どちらにせよたかが知れてる!)、式木デリゲートキャッシュは採用の方向で。スッキリしてて、C#らしい美しさなところも好き。で、まあ一応、速度を気にしてのことなので、ベンチマークを取ったりするわけですが、生成時間が気になる……。エクストリームにハイパフォーマンスなシリアライザを作る!とかってわけじゃないので、あんまキチキチに気にしてもしょうがないのですが、でもちょっと気になる。Boxingが~、とかも少し、でもそれは放置として。
リフレクションは遅いから情報をキャッシュするとかのお話 - Usa*Usa日記 [雑記] 動的コード生成のパフォーマンス (C# によるプログラミング入門) 動的プロキシなViewModelの実装とパフォーマンスの比較(MVVMパターン) - the sea of fertility 効率の良い実行時バインディングとインターフェイス指向プログラミングでの boxing の回避テクニック - NyaRuRuの日記 パフォーマンスのための Delegate, LCG, LINQ, DLR (を後で書く) - NyaRuRuの日記 インライン・メソッド・キャッシュによる動的ディスパッチ高速化 - @IT ByRef parameter and C# - 猫とC#について書くmatarilloの日記 c# - How does protobuf-net achieve respectable performance? - Stack Overflow HyperDescriptor: Accelerated dynamic property access - CodeProject Making reflection fly and exploring delegates - Jon Skeet: Coding Blog patterns & practices – Enterprise Library(Data) MetaAccessor クラス (System.Data.Linq.Mapping) TypeDescriptor クラス (System.ComponentModel) AutoMapper csharp/msgpack at master from kazuki/msgpack - GitHub cli/src/MsgPack at master from yfakariya/msgpack - GitHub
ネタ元。先人が百億光年前に辿ってきた話だ、的な何か。コードは、シリアライザやマッパーはこの辺の仕組み載せてるよね、という当たりをつけて、ソースコードをPropertyInfoやDictionaryで検索してヒットした周辺を眺めるなど。で、まあ、分かったような分からないような。キャッシュの仕組みと含めて、上手くまとまったらDynamicJsonにも載せようと思っているんですが。一週間ぐらい延々と弄ってるんですが、どうも固まらなくて。この辺、コード書きの遅さに定評のある私です(キリッ。
ちなみにWP7には今のところExpression.Compileはないんですけどね!(Betaの頃はあったけど削られたよう)。SQLCE搭載でLinq to Hogeも搭載するはずなので、それと一緒に復活するはずと信じています。あとSL4相当じゃないとExpression.AssignやUnboxが使えなくてどちらにせよ困るので、MangoではSL4相当にグレードアップしてくれないと。もし一切変わらなかったら、IL生成はもとから出来ないし将来も搭載されないと思うので、適当に何かでお茶濁しますか。