HIDORI on The Web

マネージコードしか書きたくない

Archive for the ‘C# LINQ’ tag

[C#] LINQ to SQL の実行速度を3倍に加速するっ!

without comments

どーも最近、ダメな子扱いされがちな LINQ to SQL ですが、クエリ結果を(ほぼ)透過的にキャッシュすることで実行速度を約3倍に加速してみました。

使い方はひじょーにカンタン。

通常

var connectionString = Properties.Settings.Default.Database1ConnectionString;

using (var db = new DataClasses1DataContext(connectionString))
{
    var array = db.Products.Where(__ => __.Price > 500).OrderBy(_ => _.Price).Take(200).ToArray();
}

みたいに書くところを

var connectionString = Properties.Settings.Default.Database1ConnectionString;

using (var connection = new Mjollnir.Data.SqlClient.SqlConnection(connectionString, new MemoryCache()))
using (var db = new DataClasses1DataContext(connection))
{
    var array = db.Products.Where(__ => __.Price > 500).OrderBy(_ => _.Price).Take(200).ToArray();
}

のように、データベース接続を別クラスに差し替えるだけ。

実際には少しばかりの準備(=プロジェクトに上記 SqlConnection の代替えクラスおよび SqlCommand の代替えクラス、キャッシュ実装クラスを追加)は必要だけど、クエリの記述や結果の扱いに変更を加えないで済むのがミソ。

これでどれくらいのパフォーマンス向上があるかとゆーと…

Id int
Name nvarchar(100)
Price int

とゆー構成で1000項目が登録されたテーブルに対して

private static IEnumerable
 ExecuteQuery(DataClasses1DataContext db, int queries)
{
    var list = new List
();

    for (int i = 0; i < queries; i++)
    {
        list.Add(db.Products.Where(__ => __.Price > 500).OrderBy(_ => _.Price).Take(200).ToArray());
    }

    return list;
}

とゆータスク(queries = 10 に設定)を100件連続して実行した場合の実行時間は以下のとおり。

  1回目 2回目 3回目 4回目 5回目
通常の LINQ to SQL 5.453s 3.693s 2.811s 2.84s 2.827s
LINQ to SQL + サンプルキャッシュ実装 0.942s 0.909s 0.906s 0.909s 0.906s

てなカンジ。

概ね3倍のパフォーマンス向上が見られるのでありました。

↑と同じ設定のタスクを ThreadPool を使って同時並行に実行すると

  1回目 2回目 3回目 4回目 5回目
通常の LINQ to SQL 0.964s 0.972s 0.976s 1.004s 0.987s
LINQ to SQL + サンプルキャッシュ実装 0.277s 0.271s 0.27s 0.288s 0.269s

とゆー結果。あれ?思ったほど伸びてない (^^;

実行時間そのものはマルチスレッド化によって短縮されているものの、「約3倍のパフォーマンス向上」てとこには変化ナシ…

サンプルではローカルのファイルアタッチ DB を相手にしているので、そのせいかな?

ネットワーク接続の DB なら、キャッシュヒットした時の利益はひとしおなので、もうちょっと差が出るハズ。

ところで、実は「LINQ to SQL のパフォーマンスを向上する」のであれば

  • CompiledQuery を利用する
  • 結果セットをキャッシュする

のが定番コース。

なのに、なんでわざわざこんな風変わりなアプローチをしているのかとゆーと…

↑で挙げた代表的な2つの手法には

  • クエリ実行のコードの書き換えが必要(=透過的でない)
  • SqlDependency によるキャッシュエントリの効率的なエクスパイアを実行しづらい

という短所があるから。

前者は(必要なら)我慢するとしても、後者はもうちょっとどーにかしたいところ。

で、今回採用した「LINQ to SQL の DataContext に、DbConnection.CreateCommand() で DbCommand.ExecuteReader メソッドの処理を外部に移譲する DbCommand 派生クラスインスタンスを返す DbConnection 派生クラスインスタンスを渡す(長っ)」という手法なら

  • DataContext を new する時のコードをホンの少し書き換えるだけ
  • 結果セットのキャッシュを SqlCommand 相当のクラス内で行うので、SqlDependency を容易に扱える

となるワケ。(SqlDependency を使ったキャッシュ実装はまだ作ってないけど (^^;)

あと、LINQ to SQL のクエリ結果と違って、この層なら結果セットに含まれる値の型はごくシンプルなモノばかりなので

LINQ to SQL のクエリ結果を分散ストレージにキャッシュする

なんてことも理論的には可能だったりする。

てか、実のところ、今回の投稿はそれを検討するため下地だったりなんかして (^^;

なお、サンプルプロジェクトは VS2010 で作ったけど、ターゲットは .NET3.5SP1 なので、ソースファイルを抜き出してプロジェクトを再構成すれば VS2008 でも動作するはず。

Written by Hiroaki SHIBUKI

2月 3rd, 2011 at 2:40 am

Posted in 技術情報

Tagged with