Advent LINQ (23) : 事前評価可能な式

IQueryableで表現されるクエリに、事前評価可能な式が含まれることがある。

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // 検索条件を変数で与える
    var selectType = "Shipping";
    var addresses =
        from customerAddress in context.CustomerAddress
        where customerAddress.AddressType == selectType
        select customerAddress;

}

上記のselectTypeは、LINQクエリ中の条件式に与えられる。これはメソッド構文で言う所のラムダ式に変換されるため、このローカル変数は暗黙のクロージャーのメンバーフィールドとして定義される。この式のExpressionをダンプすると、以下の結果が得られる。

<Call value="value(System.Data.Entity.Core.Objects.ObjectQuery1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly).Where(customerAddress =&gt; (customerAddress.AddressType == value(ShowQueryInLinqToEntities.Program+&lt;&gt;c__DisplayClassd).selectType))">
    <Null />
    <Call value="value(System.Data.Entity.Core.Objects.ObjectQuery1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly)">
        <Constant value="value(System.Data.Entity.Core.Objects.ObjectQuery`1[ShowQueryInLinqToEntities.CustomerAddress])" />
        <Constant value="AppendOnly" />
    </Call>
    <Lambda value="customerAddress => (customerAddress.AddressType == value(ShowQueryInLinqToEntities.Program+<>c__DisplayClassd).selectType)">
        <Equal value="(customerAddress.AddressType == value(ShowQueryInLinqToEntities.Program+<>c__DisplayClassd).selectType)">
            <MemberAccess value="customerAddress.AddressType">
                <Parameter value="customerAddress" />
            </MemberAccess>
            <MemberAccess value="value(ShowQueryInLinqToEntities.Program+<>c__DisplayClassd).selectType">
                <Constant value="value(ShowQueryInLinqToEntities.Program+<>c__DisplayClassd)" />
            </MemberAccess>
        </Equal>
        <Parameter value="customerAddress" />
    </Lambda>
</Call>

少々長い(そして、ノード毎の出力がいい加減なので醜い)が、Equalノード下に二つのMemberAccessノードがあり、片方はcustomerAddress.AddressTypeを示している。と言う事は、もう片方のConstantノードが、クロージャーのメンバーフィールドを示していると言えそうだ。
このLINQクエリは、例によって「人類の英知」を使えば、以下のように単純化できる。

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // 検索条件をクエリの式中に直接埋め込む
    var addresses =
        from customerAddress in context.CustomerAddress
        where customerAddress.AddressType == "Shipping"
        select customerAddress;
}

こうすると:

<Call value="value(System.Data.Entity.Core.Objects.ObjectQuery1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly).Where(customerAddress =&gt; (customerAddress.AddressType == &quot;Shipping&quot;))">
    <Null />
    <Call value="value(System.Data.Entity.Core.Objects.ObjectQuery1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly)">
        <Constant value="value(System.Data.Entity.Core.Objects.ObjectQuery`1[ShowQueryInLinqToEntities.CustomerAddress])" />
        <Constant value="AppendOnly" />
    </Call>
    <Lambda value="customerAddress => (customerAddress.AddressType == "Shipping")">
        <Equal value="(customerAddress.AddressType == "Shipping")">
            <MemberAccess value="customerAddress.AddressType">
                <Parameter value="customerAddress" />
            </MemberAccess>
            <Constant value=""Shipping"" />
        </Equal>
        <Parameter value="customerAddress" />
    </Lambda>
</Call>

Equalノード配下の二つ目のノードが、MemberAccessから直接的なConstantの文字列に置き換わっている。
さて、最適化の概要を見たが、このような事が出来れば良い事は分かるが、実際必要なのだろうか? 今回の最適化作業は前回と異なり、「行わなければならない」最適化となる。もしこれを実行しないと、LINQプロバイダーがExpressionを処理する際に、「customerAddress.AddressType == selectType」という式をサービス側に解釈可能な形で変換しなければならない。selectTypeはクロージャーのメンバーフィールドだ。サービスはこのクロージャーのメンバーフィールドに「リモート」から直接アクセス出来るだろうか?
もちろん、出来る訳がない。出来ないとしたら、取りうる手段は以下の二つだ。

  • クエリのエラーとする。
    但し、エラーとしてしまうと、クエリを非常に簡潔に書かなくてはならないという制約が生じる。上記のような、ちょっとした(ローカル変数への参照)式でさえ、NGとなってしまう。これは厳しい制約だ。
  • 事前にこのフィールドを解釈し、サービスに提供出来る検索条件にする。
    もし、注目している式がConstantな値(固定値)に変換出来るのであれば、あらかじめ変換してしまえば良い。上記の例のように、ローカル変数への参照(クロージャーのメンバーフィールドへの参照)を事前に解釈して、単なる「Shipping」文字列に出来てしまえば、サービス側に送ることが可能となる。

そういったわけで、Constantに事前変換可能な式を見つけ出して、変換する事が必要となる。この操作を行うのは、「Evaluator」クラスだ。このクラスは内部に「Nominator」クラスと「SubtreeEvaluator」クラスを含む。どちらもExpressionVisitorクラスを継承していて、Expressionの探索を行う。

最初のステップ (Nominator)

BeforePartialEval1000
「Nominator」クラスは、Expressionを探索して、Expressionノードの末端(Leaf)から逆順で、Constantに変換可能かを見る。変換可能かどうかは、そのExpressionノードが「Parameter」かどうかで判断する。Parameterでなければ変換可能とみるが、ノードのチェインのどこかにParameterノードが存在すれば、そのノードより上(Root側)のノードは、全て変換不可とみなされる。
Parameterノードが含まれているツリーの枝は、何らかのメソッド呼び出しを実行しないと、結果が得られない可能性がある。しかし、LINQクエリ中にメソッド呼び出しが記述されていると言う事は、単にサービスの向うの機能を仮想的に表現したものであるかもしれない。そうであれば、ローカル環境で実行しても無意味となるので、実行してConstantに変換する事は出来ない。
このような判断を下しつつ、変換可能なExpressionノードをHashSetに収集する。
#上の図では、変換不可とみなされたノードにチェックを付けている。
#また、HashSetに収集されるExpressionを、〇で囲った。

変換のステップ (SubtreeEvaluator)

AfterPartialEval1000
Nominatorで収集したExpression群は変換可能候補のリストであるので、SubtreeEvaluatorクラスの探索でこのリストをチェックし、該当するノードを見つけたらそのノード以下をConstantに変換する。既にConstantであるExpressionやnullのExpressionは無視する。
変換方法は簡単だ。まず、そのExpressionをラムダ式に変換する(Expression.Lambdaメソッドを使う)。次に、そのLambdaExpressionをコンパイルし、出来上がったデリゲートをコールする。すると、実際に式が実行されて、式の評価結果が固定値として返って来る。あとは、Expression.Constantメソッドで、Expressionに変換すれば完了だ。
ExpressionVisitorは、探索だけではなく、Expressionツリーの変更もサポートする。本来、Expressionはイミュータブル(変更出来ないクラス・例えばSystem.Stringのような)なので変更出来ないが、Visitメソッドの戻り値として新しいExpressionを返すことで、Expressionのツリーを変更(サブツリーの置き換え)する事が出来る。
探索だけ行う場合は、元のExpressionを返せば、式の変形は行われない。なので、Expression.Constantで新しいExpressionを作ったら、単にそれを返却すればよい。これで注目している式をConstantに置き換える事が出来た。

投稿者:

kekyo

A strawberry red slime mold. Likes metaprogramming. MA. Bicycle rider. http://amzn.to/1SeuUwD