Archive for the ‘C# LINQ’ tag
[C#] LINQ to SQL の実行速度を3倍に加速するっ!
どーも最近、ダメな子扱いされがちな 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 IEnumerableExecuteQuery(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 でも動作するはず。