RSS

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

23 12月

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.ObjectQuery`1[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.ObjectQuery`1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly)">
		<Constant value="value(System.Data.Entity.Core.Objects.ObjectQuery`1[ShowQueryInLinqToEntities.CustomerAddress])" />
		<Constant value="AppendOnly" />
	</Call>
	<Lambda value="customerAddress =&gt; (customerAddress.AddressType == value(ShowQueryInLinqToEntities.Program+&lt;&gt;c__DisplayClassd).selectType)">
		<Equal value="(customerAddress.AddressType == value(ShowQueryInLinqToEntities.Program+&lt;&gt;c__DisplayClassd).selectType)">
			<MemberAccess value="customerAddress.AddressType">
				<Parameter value="customerAddress" />
			</MemberAccess>
			<MemberAccess value="value(ShowQueryInLinqToEntities.Program+&lt;&gt;c__DisplayClassd).selectType">
				<Constant value="value(ShowQueryInLinqToEntities.Program+&lt;&gt;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.ObjectQuery`1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly).Where(customerAddress =&gt; (customerAddress.AddressType == &quot;Shipping&quot;))">
	<Null />
	<Call value="value(System.Data.Entity.Core.Objects.ObjectQuery`1[ShowQueryInLinqToEntities.CustomerAddress]).MergeAs(AppendOnly)">
		<Constant value="value(System.Data.Entity.Core.Objects.ObjectQuery`1[ShowQueryInLinqToEntities.CustomerAddress])" />
		<Constant value="AppendOnly" />
	</Call>
	<Lambda value="customerAddress =&gt; (customerAddress.AddressType == &quot;Shipping&quot;)">
		<Equal value="(customerAddress.AddressType == &quot;Shipping&quot;)">
			<MemberAccess value="customerAddress.AddressType">
				<Parameter value="customerAddress" />
			</MemberAccess>
			<Constant value="&quot;Shipping&quot;" />
		</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に置き換える事が出来た。

広告
 
コメントする

投稿者: : 2013/12/23 投稿先 .NET, LINQ

 

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中

 
%d人のブロガーが「いいね」をつけました。