ラベル 式木 の投稿を表示しています。 すべての投稿を表示
ラベル 式木 の投稿を表示しています。 すべての投稿を表示

2015年3月25日

IQueryable はタイプセーフではない

Entity Framework (EF) 使いにとっては常識かもしれないが、IQueryable は残念ながらタイプセーフではない。
より正確に言うと、式木の構築はタイプセーフだが、IQueryable.Provider.Execute による式木の実行はタイプセーフでなくなる。

なぜなら、IQueryProvider.Execute の実装が、すべてのメソッドに対応しているわけではないため。

例えば EF の場合、IQueryProvider.Execute は式木から SQL を構築して DB に投げるが、式木内に SQL 変換が考慮されてないメソッドがあった場合、エラーを発生させるしかない。
特に自作メソッドについては対応しようがないため、すべてエラーとなる。

[EF のエラー発生サンプル]

// テーブル定義
public partial class Customer
{
    public string Id { get; set; }
    public string Name { get; set; }
}

// 実行処理 (クラス内)
public static void Sample()
{
    using( var context = new SampleEntities() ){
        var query =
            from item in context.Customer
            select item.Name.ToUpperInvariant();
        // ToUpperInvariant() は LINQ to Entities 未対応
        // ToUpper() は対応してる (EF 5.0 で確認)

        foreach( var item in query ) Console.WriteLine(item);
        // System.NotSupportedException 発生
    }
}

例外的に、Queryable.AsQueryable から生成できる EnumerableQuery は、ほぼタイプセーフになっている。
これの IQueryProvider.Execute の実装は、式木内の Queryable クラスのメソッドを Enumerable クラスの同名のメソッドに置換した後、コンパイルしてラムダ式を作成し、そのまま実行している。
つまり、LINQ to Objects に変換して実行するだけなので、自作メソッドでも何でも使用可能。

参考情報

LINQ to Entities で使用可能なメソッドについては、下記参照。
上記にないものについては、EntityFunctions や SqlFunctions (SQL Server 専用) を使用する。

LINQ to Entities で自作メソッドを SQL にマップする方法も一応ある。
LINQ to Entities が実際に実行できるかを確認したい場合は、LINQPad がオススメ。
というか競合してるアプリが他にない・・・

2015年3月21日

IQueryable の仕組みについて

Entity Framework (以下、EF) の LINQ to Entities を実現するための IQueryable インターフェイスについて、今まであまり理解してなかったので MSDN とソースからお勉強。
理解してなくても使える EF は凄い。

IQueryable と IQueryProvider


public interface IQueryable : IEnumerable {
    Expression Expression { get; }
    Type ElementType { get; }
    IQueryProvider Provider { get; }
}
public interface IQueryable<out T> : IEnumerable<T>, IQueryable { }

public interface IQueryProvider{
    IQueryable CreateQuery(Expression expression);
    IQueryable<TElement> CreateQuery<TElement>(Expression expression);

    object Execute(Expression expression);
    TResult Execute<TResult>(Expression expression);
}

ソースの通りなのだが、とりあえず要点をまとめると
  • IQueryable.Expression は式木(式ツリー)を保持 (IQueryable の本体)
  • IQueryable.Provider は IQueryProvider を提供
  • IQueryable の通常版・ジェネリック版の違いは、継承元の IEnumerable がジェネリック版かどうか
  • IQueryProvider.CreateQuery(Expression) は引数の式木から IQueryable インスタンスを作成する
    (主に IQueryable の拡張メソッドで使用される)
  • IQueryProvider.Execute(Expression) は引数の式木を実行する
    (LINQ to Entities なら式木から SQL を構築して実行し、結果を返す)

IQueryable と IQueryProvider の実装方法は下記参照。

Queryable 拡張メソッド(IQueryable の標準クエリ演算子)

System.Linq.Queryable クラスの拡張メソッドの処理についてソースから説明。

[Queryable.Select の実装 (見易さのため一部整形)]

public static IQueryable<TResult> Select<TSource,TResult>(
    this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector
) {
    if (source == null) throw Error.ArgumentNull("source");
    if (selector == null) throw Error.ArgumentNull("selector");

    return source.Provider.CreateQuery<TResult>( 
        Expression.Call(
            null,
            ((MethodInfo)MethodBase.GetCurrentMethod())
                .MakeGenericMethod(typeof(TSource), typeof(TResult)), 
            new Expression[] { source.Expression, Expression.Quote(selector) }
        )
    );
}
[行毎の説明]
4-5 引数の null チェック
7 IQueryable インスタンスを新たに作成して return
※ IQueryable はイミュータブルなので、式木を変更するには新規作成が必要
8 IQueryProvider.CreateQuery の引数 : Expression.Call はメソッド呼び出しの式木を作成
9 Expression.Call の第 1 引数 : static メソッドを呼び出すので null を指定
10-11 Expression.Call の第 2 引数 : 自身 (Queryable.Select) を呼び出しメソッドとして指定
12 Expression.Call の第 3 引数 : 呼び出すメソッドの引数を式木で指定
1 個目の引数は IQueryable.Expression を使用し、2 個目の引数は selector 引数を使用
※ Expression.Quote は式木型引数を表すために使うものらしい

式木に触ったことが無い場合は意味不明かもしれないが、やってることは単純で、自身のメソッド呼び出しを式木に変換しているだけである。
Queryable クラスの他の拡張メソッドも、一部を除いてほぼ同じ実装。
(こういう実装の場合、CSS のクラスみたくメソッドの定義に対してカンマ区切りが使えれば理想かも)

また、式木 (Expression 型) は、ToString() で簡単に中身を見ることができる。

[Where と Select を使った式木の表示]

var source = new[]{ 0, 1, 2, 3, 4, 5 };
// 純粋な IEnumerable に対する AsQueryable() は EnumerableQuery を生成
var query =
    from n in source.AsQueryable()
    where n % 2 == 1
    select n * n;

Console.WriteLine(query.Expression);
Console.WriteLine(string.Join(",", query));
[結果]
System.Int32[].Where(n => ((n % 2) == 1)).Select(n => (n * n))
1,9,25

まとめ

  • IQueryable は式木を構築するためのインターフェース
  • Queryable 拡張メソッドは式木を簡単に構築するためのユーティリティ

Expression.Quote の存在に疑問を持った場合は下記が参考になる。

2014年9月2日

コンパイラをごまかすキャスト

※実質、.net - C# non-boxing conversion of generic enum to int? - Stack Overflow の紹介記事です。

ジェネリック型パラメータと Enum の相性は悪い。
型パラメータの制約として、Enum クラスを指定できない。
Enum に対する制約は、せいぜい下記のようにしかできない。(ゆるい制約だとstructだけの場合も)
[サンプル1 : Enum に対する制約]

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
}

問題は、型パラメータで受け取った Enum 値を int などにキャストする場合である。
[サンプル1]で value を int へキャストしようとすると、コンパイラに怒られる。
TEnum の制約では、int へ変換可能かどうかを判断できないためである。
int type conversion error

object キャスト

これを解決する最も簡単な方法は、一度 object へキャストすることである。

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
    var i = (int)(object)value;
}

ただし、この方法はコードの通りボックス化(ボクシング)→ボックス化解除(アンボクシング)であるため、あまりパフォーマンス的によろしくない。

動的ラムダ式でキャスト

ボックス化を回避できる方法として、式木で動的ラムダ式を構築してキャストする方法がある。
※このコードを少し変更(Get メソッドの処理を静的コンストラクタに移動)。

public static class CastTo<T>
{
    private static class Cache<S>
    {
        static Cache()
        {
            // 次のラムダ式を式木で構築
            // (S s) => (T)s
            var p = Expression.Parameter(typeof(S));
            var c = Expression.ConvertChecked(p, typeof(T));
            Caster = Expression.Lambda<Func<S, T>>(c, p).Compile();
        }
        internal static readonly Func<S, T> Caster;
    }

    public static T From<S>(S source)
    {
        return Cache<S>.Caster(source);
    }
}
[使用例]

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
    var i = CastTo<int>.From(value);
}

Expression.ConvertChecked でキャストするだけのラムダ式を構築している。
Expression.ConvertChecked を使うことで、コンパイラのキャスト可能チェックをスルーできる。
ちなみにオーバーフロー例外を発生させたくない場合は Expression.Convert を使う。

動的IL でキャスト

ここにもあるのだが、キャスト処理を動的IL で構築することで、ボックス化を回避できる。

public static class IlCastToInt32<T>
{
    static IlCastToInt32()
    {
        var method = new DynamicMethod(
            "IlCastToInt32Caster", typeof(int), new[] { typeof(T) });
        var il = method.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Conv_Ovf_I4);
        il.Emit(OpCodes.Ret);
        Caster = (Func<T, int>)method.CreateDelegate(typeof(Func<T, int>));
    }
    public static readonly Func<T, int> Caster;
}

// クラスの型パラメータを省略するための拡張メソッド
public static class IlCastExtensions
{
    public static int CastToInt32<T>(this T value)
    {
        return IlCastToInt32<T>.Caster(value);
    }
}
[使用例]

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
    var i = value.CastToInt32();
}

戻り値を決め打ちする必要があるため、動的ラムダ式より汎用性が下がる。
その他の型に変換したい場合は、OpCodes を参照。
また、型パラメータとして渡すもの(ここでは Enum)は public である必要がある。
public でないと TypeAccessException が発生する。

パフォーマンス

[測定コード]

static class TestEnumCast
{
    static void Main(string[] args)
    {
        var flag = int.Parse(args[0]); // 1 ~ 5
        var loopCount = int.Parse(args[1]);
        var source = MuseEnumerable().Take(loopCount).ToArray();
        var temp = new List<int>(loopCount);

        if( flag > 3 ){
            // 4,5 はイニシャルコスト無しの設定
            // -> 一度実行してキャッシュを作成しておく
            flag -= 2;
            CastTo<int>.From(Muse.None);
            Muse.None.CastToInt32();
        }

        var sw = new Stopwatch();
        sw.Start();
        switch( flag ){
        case 1:
            foreach( var item in source ) temp.Add((int)(object)item);
            break;
        case 2:
            foreach( var item in source ) temp.Add(CastTo<int>.From(item));
            break;
        case 3:
            // 拡張メソッドだとメソッド 1 個分のコストがかかるため、直呼び
            foreach( var item in source ) temp.Add(IlCastToInt32<Muse>.Caster(item));
            break;
        }
        sw.Stop();
        Console.WriteLine(sw.Elapsed);
    }

    static IEnumerable<Muse> MuseEnumerable()
    {
        while( true ){
            yield return Muse.Honoka;
            yield return Muse.Kotori;
            yield return Muse.Umi;
        }
    }
}

[Flags]
public enum Muse
{
    None = 0x0,
    Honoka = 0x1,
    Kotori = 0x2,
    Umi = 0x4
}

[結果]
loopCount object 動的ラムダ式 動的IL
イニシャルコスト有 10000 0.0003435 0.0053873 0.0031249
イニシャルコスト無 0.0001409 0.0001416
イニシャルコスト有 100000 0.0035345 0.0067660 0.0044378
イニシャルコスト無 0.0014422 0.0014731
※コマンドプロンプトで実施(csc /optimize)
※10回の算術平均、単位は[s]

結果をまとめると下記。(左の方が高性能)
キャスト : 動的ラムダ式 ≒ 動的IL > object
イニシャルコスト : 動的IL > 動的ラムダ式

キャスト処理に関しては、ボックス化を回避することで 2 倍以上の性能になる。
動的ラムダ式と動的IL で差はほぼないため、
汎用性や型パラメータの public 制限を考慮すると動的ラムダ式、イニシャルコストが少ない方がいいなら動的IL となる。
また、動的IL は拡張メソッドから呼び出すと、わずかだがその分のコストがかかる。

ただ、イニシャルコスト >>> object キャスト 1 回 なので、条件によっては無意味な工夫になる。

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0
Intel(R) Celeron(R) CPU G530 @ 2.40GHz/DDR3 8GB(4GB x 2)