[解決済み] Entity Frameworkの非同期処理には10倍の時間がかかる
質問
私は、データベースを処理するためにEntity Framework 6を使用しているMVCサイトを持っています。私は、すべてが非同期コントローラとして実行され、データベースへの呼び出しがそれらの非同期対応として実行されるようにそれを変更して試してきました(例:ToList()ではなく、ToListAsync())。
私が抱えている問題は、単にクエリを非同期に変更するだけで、信じられないほど遅くなることです。
次のコードは、データコンテキストから "Album" オブジェクトのコレクションを取得し、かなり単純なデータベース結合に変換されます。
// Get the albums
var albums = await this.context.Albums
.Where(x => x.Artist.ID == artist.ID)
.ToListAsync();
作成されるSQLはこちらです。
exec sp_executesql N'SELECT
[Extent1].[ID] AS [ID],
[Extent1].[URL] AS [URL],
[Extent1].[ASIN] AS [ASIN],
[Extent1].[Title] AS [Title],
[Extent1].[ReleaseDate] AS [ReleaseDate],
[Extent1].[AccurateDay] AS [AccurateDay],
[Extent1].[AccurateMonth] AS [AccurateMonth],
[Extent1].[Type] AS [Type],
[Extent1].[Tracks] AS [Tracks],
[Extent1].[MainCredits] AS [MainCredits],
[Extent1].[SupportingCredits] AS [SupportingCredits],
[Extent1].[Description] AS [Description],
[Extent1].[Image] AS [Image],
[Extent1].[HasImage] AS [HasImage],
[Extent1].[Created] AS [Created],
[Extent1].[Artist_ID] AS [Artist_ID]
FROM [dbo].[Albums] AS [Extent1]
WHERE [Extent1].[Artist_ID] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=134
このクエリはそれほど複雑なものではありませんが、SQLサーバーが実行するのに6秒近くかかっています。SQL Server Profiler は、完了までに 5742 ミリ秒かかったと報告しています。
もし私が私のコードを
// Get the albums
var albums = this.context.Albums
.Where(x => x.Artist.ID == artist.ID)
.ToList();
次に、まったく同じ SQL が生成されますが、SQL Server Profiler によると、これはわずか 474ms で実行されます。
データベースには、"Albums" テーブルに約 3500 行があり、これは実際にはそれほど多くなく、また "Artist_ID" 列にインデックスがあるので、かなり高速であるはずです。
非同期にはオーバーヘッドがあることは知っていますが、物事を10倍遅くすることは、私には少し急なことのように思えます! 私はどこで間違っているのでしょうか?
どのように解決するのですか?
私はこの質問が非常に興味深いものであると感じました。
async
をAdo.NetとEF 6でいたるところで使用しているからです。私は誰かがこの質問に対する説明を与えることを期待していましたが、それは起こりませんでした。そこで、私の側でこの問題を再現してみました。私はあなたの何人かがこれを面白いと思うことを願っています。
最初の良いニュースは:私はそれを再現した:)。そして、その差は非常に大きいです。ファクター8で ...
最初に、私は何か
CommandBehavior
を扱っているのではないかと疑いました。
面白い記事を読んだので
について
async
をアドで、こう言っています。
"非シーケンシャルアクセスモードは行全体のデータを保存しなければならないので、サーバから大きな列(varbinary(MAX), varchar(MAX), nvarchar(MAX) or XMLなど)を読み込む場合は問題を起こす可能性があります)"。
私が疑っていたのは
ToList()
の呼び出しは
CommandBehavior.SequentialAccess
で、非同期呼び出しは
CommandBehavior.Default
(にする必要があります(非シーケンシャル、これは問題を引き起こす可能性があります)。そこで、EF6のソースをダウンロードして、あちこちにブレークポイントを置いてみました(ここで
CommandBehavior
が使われているところ)。
結果:
何もない
. すべての呼び出しは
CommandBehavior.Default
.... そこで、何が起こっているのかを理解するためにEFのコードに踏み込もうとしたところ...うーん......。私はこのような委譲されたコードを見たことがありません、すべてが怠惰に実行されているようです...
そこで、何が起こっているのかを理解するために、プロファイリングをしてみました...
そして、何かわかったような気がします...
私がベンチマークしたテーブルを作成するためのモデルです。その中には3500行があり、それぞれに256Kbのランダムデータがあります
varbinary(MAX)
. (EF6.1-CodeFirstの場合-)
コードプレックス
) :
public class TestContext : DbContext
{
public TestContext()
: base(@"Server=(localdb)\\v11.0;Integrated Security=true;Initial Catalog=BENCH") // Local instance
{
}
public DbSet<TestItem> Items { get; set; }
}
public class TestItem
{
public int ID { get; set; }
public string Name { get; set; }
public byte[] BinaryData { get; set; }
}
そして、テストデータの作成とEFのベンチマークに使用したコードは以下の通りです。
using (TestContext db = new TestContext())
{
if (!db.Items.Any())
{
foreach (int i in Enumerable.Range(0, 3500)) // Fill 3500 lines
{
byte[] dummyData = new byte[1 << 18]; // with 256 Kbyte
new Random().NextBytes(dummyData);
db.Items.Add(new TestItem() { Name = i.ToString(), BinaryData = dummyData });
}
await db.SaveChangesAsync();
}
}
using (TestContext db = new TestContext()) // EF Warm Up
{
var warmItUp = db.Items.FirstOrDefault();
warmItUp = await db.Items.FirstOrDefaultAsync();
}
Stopwatch watch = new Stopwatch();
using (TestContext db = new TestContext())
{
watch.Start();
var testRegular = db.Items.ToList();
watch.Stop();
Console.WriteLine("non async : " + watch.ElapsedMilliseconds);
}
using (TestContext db = new TestContext())
{
watch.Restart();
var testAsync = await db.Items.ToListAsync();
watch.Stop();
Console.WriteLine("async : " + watch.ElapsedMilliseconds);
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
while (await reader.ReadAsync())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReaderAsync SequentialAccess : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
while (await reader.ReadAsync())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReaderAsync Default : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
while (reader.Read())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReader SequentialAccess : " + watch.ElapsedMilliseconds);
}
}
using (var connection = new SqlConnection(CS))
{
await connection.OpenAsync();
using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
{
watch.Restart();
List<TestItem> itemsWithAdo = new List<TestItem>();
var reader = cmd.ExecuteReader(CommandBehavior.Default);
while (reader.Read())
{
var item = new TestItem();
item.ID = (int)reader[0];
item.Name = (String)reader[1];
item.BinaryData = (byte[])reader[2];
itemsWithAdo.Add(item);
}
watch.Stop();
Console.WriteLine("ExecuteReader Default : " + watch.ElapsedMilliseconds);
}
}
通常のEF呼び出しの場合(
.ToList()
) では、プロファイリングは "normal" のように見え、読みやすくなっています。
ここでは、ストップウォッチで得られた8.4秒を見つけることができます(プロファイリングによってperfsが遅くなります)。また、呼び出しパスに沿って HitCount = 3500 が見つかり、これはテストでの 3500 行と一致しています。TDSパーサ側では、118 353の呼び出しがあり、事態は悪化し始めました。
TryReadByteArray()
メソッドで 118 353 回の呼び出しがあり、バッファリング ループが発生したためです。(各
byte[]
に対して平均 33.8 回の呼び出し)。
については
async
の場合、本当に本当に違います......。まず
.ToListAsync()
の呼び出しは ThreadPool 上でスケジュールされ、その後待機します。ここでは何も驚くことはありません。しかし、今度は
async
の地獄は、ThreadPool にあります。
まず、最初のケースでは、完全な呼び出しパスに沿ってちょうど 3500 のヒット カウントがありましたが、ここでは 118 371 です。さらに、スクリーンショットに載せていないすべての同期呼び出しを想像してください...
次に、最初のケースで、私たちは "ちょうど 118 353" の呼び出しを持っていました。
TryReadByteArray()
メソッドへの呼び出しが 2,050 210 回もありました。これは17倍です... (大きな 1Mb の配列を使用したテストでは、160 倍になります)
さらに、次のようなこともあります。
-
120 000
Task
作成されたインスタンス -
727 519
Interlocked
コール -
290 569
Monitor
コール -
98 283
ExecutionContext
インスタンス、264 481 キャプチャーを含む -
208 733
SpinLock
コール
私の推測では、バッファリングは、TDSからデータを読もうとする並列タスクで、非同期方法(そして良いものではない)で作られていると思います。バイナリ データを解析するために、あまりにも多くのタスクが作成されます。
予備的な結論として、非同期は素晴らしく、EF6 も素晴らしいですが、現在の実装での EF6 の非同期の使用は、パフォーマンス面、スレッド面、CPU 面で大きなオーバーヘッドを追加しています (
EF6 の非同期の使用では 12% CPU 使用量が増加します)。
ToList()
の場合で 12%、ToListAsync
の場合、8~10倍の長時間の作業となります...。私は古いi7 920でそれを実行します)。
いくつかのテストを行っている間、私は次のことを考えていました。 この記事をもう一度 について考えていて、見落としていることに気づきました。
.Net 4.5 の新しい非同期メソッドについては、1 つの顕著な例外を除いて、その動作は同期メソッドとまったく同じです。非シーケンシャル モードでの ReadAsync です。
なんだ!
そこで、ベンチマークを拡張して、Ado.Netを通常/非同期呼び出しに含め、さらに
CommandBehavior.SequentialAccess
/
CommandBehavior.Default
そして、ここで大きな驚きが ! :
Ado.Netで全く同じ動作をしています !!! 顔面蒼白です...
私の決定的な結論は
: EF 6 の実装にバグがある。これは
CommandBehavior
を
SequentialAccess
を含むテーブルに対して非同期呼び出しが行われた場合、そのテーブルには
binary(max)
カラムを含むテーブルに対して非同期呼び出しが行われた場合です。Taskを作りすぎて処理が遅くなるという問題は、Ado.Net側にあります。EFの問題は、Ado.Netを本来の意味で使えていないことです。
これで、EF6の非同期メソッドを使う代わりに、非同期でない通常の方法でEFを呼び出す必要があることがわかったと思います。
TaskCompletionSource<T>
を使用して非同期で結果を返します。
注1 : 恥ずかしい間違いがあったので、投稿を編集しました...。最初のテストはローカルではなくネットワーク上で行ったので、帯域が制限されているために結果が歪んでしまいました。以下は更新された結果です。
注2 : 私は自分のテストを他のユースケースに拡張しませんでした (例 :
nvarchar(max)
など)には拡張していませんが、同じ挙動が起こる可能性はあります。
注3 : 何か通常の
ToList()
の場合、12%の CPU (1/8 of my CPU = 1 logical core) です。通常と異なるのは
ToListAsync()
の場合、まるでスケジューラがすべてのトレッドを使用できないかのようです。これはおそらく、作成されたタスクが多すぎるか、TDS パーサーのボトルネックによるものだと思いますが、私は知りません...
関連
-
[解決済み】統合マネージドパイプラインモードで適用されないASP.NETの設定が検出された
-
[解決済み】Entity FrameworkからのSqlException - セッション内で他のスレッドが動作しているため、新しいトランザクションは許可されません。
-
[解決済み】Moqを使用してメソッド呼び出しを検証する
-
[解決済み] 不変量名 'System.Data.SqlClient' を持つ ADO.NET プロバイダに対応する Entity Framework プロバイダが見つかりませんでした。
-
[解決済み] Entity Framework 5 レコードを更新する
-
[解決済み] Entity FrameworkとLINQ to SQLの比較
-
[解決済み] Entity Frameworkにおける最速の挿入方法
-
[解決済み] Entity Frameworkで生成されたSQLを表示するにはどうすればよいですか?
-
[解決済み] Entity Framework - 複数レベルのプロパティを含める
-
[解決済み】複数の非同期タスクを実行し、すべてのタスクが完了するのを待つ
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】GDI+、JPEG画像をMemoryStreamに変換する際にジェネリックエラーが発生しました。
-
[解決済み】ここで「要求URIに一致するHTTPリソースが見つかりませんでした」となるのはなぜですか?
-
[解決済み】パディングが無効で、削除できない?
-
[解決済み】SmtpException: トランスポート接続からデータを読み取れません:net_io_connectionclosed
-
[解決済み】ソケットのアドレス(プロトコル/ネットワークアドレス/ポート)は、通常1つしか使用できない?
-
[解決済み] [Solved] アセンブリ System.Web.Extensions dll はどこにありますか?
-
[解決済み】MetadataException: 指定されたメタデータ・リソースをロードできない
-
[解決済み】Visual studio 2019がデバッグ時にフリーズする件
-
[解決済み】「...は'型'であり、与えられたコンテキストでは有効ではありません」を解決するにはどうすればよいですか?(C#)
-
[解決済み】Nullableオブジェクトは値を持たなければならない?