2015年4月3日

Entity Framework のパフォーマンス #1 LINQ to Entities クエリ

Entity Framework (EF) の LINQ to Entities クエリの 1 回目(コールドクエリ)は、かなり遅い。
1 回目なので DB アクセスも当然遅いのだが、それだけでなく、プログラム側でコストの高い初期化処理を行っているため、遅くなっている。

どんな処理を行っているかは下記参照。

パフォーマンス

いくつかの簡単な LINQ to Entities クエリの SQL 構築時間と実行時間を測定してみる。
[環境]
  • Windows 7 64bit
  • Visual Studio 2010 SP1
  • .NET 4.5.2 (TargetFramework は 4.0)
  • Entity Framework 5.0 + Database First + DbContext
    ※ EF5.0 の理由は、VS2010 で DbContext Generator が提供されている最終バージョンのため
    ※ ObjectContext はほぼ使わないと思うのでナシ
    (DbContext は ObjectContext のラッパーなので性能は ObjectContext の方が少し上のはず)

[測定コード]

// テーブル定義
public class KeyTable1
{
    public string Key { get; set; }
    public string Name { get; set; }
    public DateTime Register { get; set; }
}
public class KeyTable2
{
    public string Key { get; set; }
    public string Name { get; set; }
    public DateTime Register { get; set; }
}
public class KeyTable3
{
    public string Key { get; set; }
    public string Name { get; set; }
    public DateTime Register { get; set; }
}

// 測定処理
static class EfTest
{
    // KeyTable1 の select の SQL 構築時間
    public static void Key1Select()
    {
        using( var context = new TestEntities() ){
            var query = from item in context.KeyTable1 select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
/* 構築される SQL :
SELECT
[Extent1].[Key] AS [Key],
[Extent1].[Name] AS [Name],
[Extent1].[Register] AS [Register]
FROM [dbo].[KeyTable1] AS [Extent1] */
        }
    }

    // KeyTable2 の select の SQL 構築時間
    public static void Key2Select()
    {
        using( var context = new TestEntities() ){
            var query =from item in context.KeyTable2 select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    // KeyTable3 の select の SQL 構築時間
    public static void Key3Select()
    {
        using( var context = new TestEntities() ){
            var query = from item in context.KeyTable3 select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    // KeyTable1 の select-where の SQL 構築時間
    public static void Key1SelectWhere()
    {
        using( var context = new TestEntities() ){
            var query =
                from item in context.KeyTable1
                where item.Key == "1"
                select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    // KeyTable1 の select-where-in (自動コンパイル対象外) の SQL 構築時間
    public static void Key1SelectWhereIn()
    {
        var keys = new[] { "1" };
        using( var context = new TestEntities() ){
            var query =
                from item in context.KeyTable1
                where keys.Contains(item.Key)
                select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    //  KeyTable1 の select の SQL 実行時間
    public static void ExecuteKey1Select()
    {
        using( var context = new TestEntities() ){
            var query = from item in context.KeyTable1 select item;
            Measurement.ConsoleElapsedTime(() => query.AsEnumerable().GetEnumerator());
        }
    }
}

// 経過時間測定ユーティリティ
static class Measurement
{
    public static void ConsoleElapsedTime(Action act)
    {
        var caller = new StackFrame(1);
        var watch = Stopwatch.StartNew();
        act();
        watch.Stop();
        Console.WriteLine("[{0}] {1}", caller.GetMethod().Name, watch.Elapsed);
    }
}

実験1. 同じテーブルに対する select

Key1Select × 5
[Key1Select] 00:00:00.1411941
[Key1Select] 00:00:00.0002129
[Key1Select] 00:00:00.0001244
[Key1Select] 00:00:00.0001124
[Key1Select] 00:00:00.0001150
1 回目はとにかく遅い。
2 回目は色々なキャッシュが効いて速くなる。(この結果だと 663 倍)
3 回目も少し速くなり、以降横ばい。

実験2. 複数のテーブルそれぞれに対する select

Key1Select × 3 → Key2Select × 3 → Key3Select × 3
[Key1Select] 00:00:00.1421443
[Key1Select] 00:00:00.0002142
[Key1Select] 00:00:00.0001231
[Key2Select] 00:00:00.0072696
[Key2Select] 00:00:00.0001616
[Key2Select] 00:00:00.0001167
[Key3Select] 00:00:00.0070160
[Key3Select] 00:00:00.0001599
[Key3Select] 00:00:00.0001321
1 回目ほどではないが、各テーブルの 1 回目もそれなりに遅い。
2 回目以降はキャッシュが効いている。

実験3. select 後の select-where

Key1Select → Key1SelectWhere × 3
[Key1Select] 00:00:00.1420541
[Key1SelectWhere] 00:00:00.0013068
[Key1SelectWhere] 00:00:00.0001667
[Key1SelectWhere] 00:00:00.0001364
Key2Select → Key1SelectWhere × 3
[Key2Select] 00:00:00.1429743
[Key1SelectWhere] 00:00:00.0074316
[Key1SelectWhere] 00:00:00.0002014
[Key1SelectWhere] 00:00:00.0001372
QueryKey1Where の 1 回目について、前者(先に同じテーブルの SQL を構築していた場合)の方が速い。
つまり、異なるクエリでも、式木が部分的に一致していればキャッシュが効くようになっている。

実験4. 自動コンパイルされないクエリ

Key1Select → Key1SelectWhereIn × 3
[Key1Select] 00:00:00.1426134
[Key1SelectWhereIn] 00:00:00.0021291
[Key1SelectWhereIn] 00:00:00.0010327
[Key1SelectWhereIn] 00:00:00.0010109
2 回目以降は早くなっているが、自動コンパイルクエリの対象外であるため、実験3. の 2 回目より遅い。

※自動コンパイルクエリについて
EF 5.0 + .NET 4.5 (TargetFramework は 4.0 でも可) の環境から、LINQ to Entities クエリは自動でコンパイルされてキャッシュされるようになっている。(以前は CompiledQuery を手動で実装する必要があった)
しかし、IN 句を構築するための Enumerable.Contains を式木に含めると自動コンパイルの対象外となる。
理由は、LINQ to Entities クエリから SQL が確定できないため。(下記参照)

実験5. SQL 構築後の SQL 実行

ExecuteKey1Select × 3
[ExecuteKey1Select] 00:00:00.1694211
[ExecuteKey1Select] 00:00:00.0020680
[ExecuteKey1Select] 00:00:00.0016365
Key1Select → ExecuteKey1Select × 3
[Key1Select] 00:00:00.1425704
[ExecuteKey1Select] 00:00:00.0296910
[ExecuteKey1Select] 00:00:00.0019183
[ExecuteKey1Select] 00:00:00.0016942
SQL 構築を事前に行っておくと、その分、SQL 実行も速くなっている。
また、1 回目のSQL 構築時間は 1回目の SQL 実行時間と比べてもかなり遅い。
(SQL 実行時間は環境依存なのであまり比較に意味はないかもしれないが・・・)

まとめ

上記から明らかなように、LINQ to Entities クエリの 1 回目は遅い。

対策としては、動作時間の長いプログラム(Web アプリやサービス)だと、起動時に全テーブルの SQL 構築処理を実行して事前キャッシュしておく、などが可能だが、1 回の実行で終わるようなプログラムの場合はどうしようもない。
そういうものに EF は使うべきではない、ということなんだろう・・・
今のところ、SQL 構築のオーバーヘッドは大きすぎると思われるので、パフォーマンスが求められる場合も EF は向いていない。

EF 5.0 以降を使用している場合、.NET 4.5 をインストールしておくことは割と重要。
複雑なクエリほど SQL 構築に時間がかかるため、自動コンパイルクエリが有効だと、2 回目以降のパフォーマンスが向上する。
CompiledQuery でクエリのキャッシュを手動管理することも可能だが、DbContext では使えないことに注意。(CompiledQuery は ObjectContext のみ対応)

クエリの初期化処理に「ビュー生成」があり、これは事前に作成しておくこと可能だが、上記の測定ではあまり効果が無かったため、省略している。
Entity Framework Power Tools を使えば、プリコンパイルビューが簡単に生成可能。

0 件のコメント:

コメントを投稿