特にasync
Ado.NetとEF 6でどこでも使用しているので、この質問は非常に興味深いものでした。誰かがこの質問について説明してくれることを望んでいましたが、実際には起こりませんでした。だから私はこの問題を私の側で再現しようとしました。これが面白いと思う人もいると思います。
最初の良いニュース:私はそれを再現しました:)そして、その違いは非常に大きいです。因数8で...
Ado に関する興味深い記事を読んで次のように言ったCommandBehavior
ので、最初に何かを処理していると疑っていました。async
「非順次アクセスモードでは行全体のデータを保存する必要があるため、サーバーから大きな列(varbinary(MAX)、varchar(MAX)、nvarchar(MAX)、XMLなど)を読み取っている場合、問題が発生する可能性があります)」
ToList()
呼び出しがCommandBehavior.SequentialAccess
非同期であることが疑われましたCommandBehavior.Default
(非順次、問題が発生する可能性があります)。そこで、EF6のソースをダウンロードし、どこにでもブレークポイントを配置しました(CommandBehavior
もちろん、使用されている場所です)。
結果:なし。すべての呼び出しはCommandBehavior.Default
...で行われるので、何が起こるかを理解するためにEFコードに足を踏み入れようとしました...そして.. ooouch ...私はそのような委任コードを見たことがないので、すべてが遅延実行されているようです...
だから私は何が起こるかを理解するためにいくつかのプロファイリングをしようとしました...
そして、私は何かを持っていると思います...
これは、ベンチマークを行ったテーブルを作成するためのモデルで、内部に3500行があり、それぞれに256 KBのランダムデータがありますvarbinary(MAX)
。(EF 6.1-CodeFirst- CodePlex):
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()
)の場合、プロファイリングは「正常」であり、読みやすいです。
ここで、ストップウォッチの8.4秒を見つけます(プロファイリングによりパフォーマンスが低下します)。また、コールパスに沿ってHitCount = 3500が見つかります。これは、テストの3500行と一致しています。TDSパーサー側でTryReadByteArray()
は、バッファリングループが発生する118 353のメソッド呼び出しを読み取ったため、状況は悪化し始めました。(byte[]
256kb ごとに平均33.8コール)
このasync
場合、それは本当に本当に異なります...最初に、.ToListAsync()
呼び出しはThreadPoolでスケジュールされ、それから待機されます。ここでは驚くべきことは何もありません。しかし、今、async
これがThreadPoolの地獄です:
まず、最初のケースでは、フルコールパスに沿ってヒットカウントが3500しかありませんでしたが、ここでは118 371です。
次に、最初のケースでは、TryReadByteArray()
メソッドへの呼び出しが「118 353」になりました。ここでは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側に大きなオーバーヘッドを追加します(ToList()
ケースとケースの20%で、ToListAsync
8〜10倍の時間で動作します...古いi7 920で実行します)。
いくつかのテストを行っているときに、この記事についてもう一度考えていたところ、見落としていることに気づきました。
「.Net 4.5の新しい非同期メソッドの場合、その動作は同期メソッドの場合とまったく同じですが、1つの注目すべき例外として、非シーケンシャルモードのReadAsyncがあります。」
何 ?!!!
私はAdo.Net定期的/非同期呼び出しでは、とに含まれるように、私のベンチマークを拡張してCommandBehavior.SequentialAccess
/ CommandBehavior.Default
、ここで大きな驚きです!:
Ado.Netとまったく同じ動作です!!! Facepalm ...
私の決定的な結論は、EF 6の実装にバグがあることです。列を含むテーブルに対して非同期呼び出しが行われると、CommandBehavior
を切り替えます。タスクを作成しすぎてプロセスが遅くなるという問題は、Ado.Net側にあります。EFの問題は、Ado.Netを本来の方法で使用しないことです。SequentialAccess
binary(max)
EF6非同期メソッドを使用する代わりに、通常の非同期ではない方法でEFを呼び出してから、a TaskCompletionSource<T>
を使用して結果を非同期で返すことをお勧めします。
注1:恥ずかしいエラーが発生したため、投稿を編集しました...ローカルではなくネットワーク経由で最初のテストを実行しましたが、帯域幅の制限により結果が歪んでいます。更新された結果は次のとおりです。
注2:テストを他のユースケース(たとえばnvarchar(max)
、大量のデータを使用する)に拡張していませんが、同じ動作が発生する可能性があります。
注3:通常のToList()
場合、12%CPU(私のCPUの1/8 = 1論理コア)です。ToListAsync()
スケジューラがすべてのトレッドを使用できなかったかのように、異常な場合はケースの最大20%です。作成されたタスクが多すぎるか、TDSパーサーのボトルネックが原因である可能性があります。