Saya menemukan pertanyaan ini sangat menarik, terutama karena saya menggunakan di async
mana-mana dengan Ado.Net dan EF 6. Saya berharap seseorang memberikan penjelasan untuk pertanyaan ini, tetapi itu tidak terjadi. Jadi saya mencoba mereproduksi masalah ini di pihak saya. Saya harap beberapa dari Anda akan menemukan ini menarik.
Berita baik pertama: Saya mereproduksinya :) Dan perbedaannya sangat besar. Dengan faktor 8 ...
Pertama saya mencurigai sesuatu yang berhubungan dengan CommandBehavior
, karena saya membaca artikel menarik tentang async
Ado, mengatakan ini:
"Karena mode akses non-sekuensial harus menyimpan data untuk seluruh baris, itu dapat menyebabkan masalah jika Anda membaca kolom besar dari server (seperti varbinary (MAX), varchar (MAX), nvarchar (MAX) atau XML) ). "
Saya mencurigai ToList()
panggilan menjadi CommandBehavior.SequentialAccess
async CommandBehavior.Default
(tidak berurutan, yang dapat menyebabkan masalah). Jadi saya mengunduh sumber-sumber EF6, dan meletakkan breakpoints di mana-mana (di CommandBehavior
mana digunakan, tentu saja).
Hasil: tidak ada . Semua panggilan dibuat dengan CommandBehavior.Default
.... Jadi saya mencoba masuk ke kode EF untuk memahami apa yang terjadi ... dan ... ooouch ... Saya tidak pernah melihat kode pendelegasian seperti itu, semuanya tampak malas dijalankan ...
Jadi saya mencoba membuat profil untuk memahami apa yang terjadi ...
Dan saya pikir saya punya sesuatu ...
Inilah model untuk membuat tabel yang saya benchmark, dengan 3.500 baris di dalamnya, dan masing-masing 256 Kb data acak 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; }
}
Dan inilah kode yang saya gunakan untuk membuat data uji, dan benchmark 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);
}
}
Untuk panggilan EF biasa ( .ToList()
), pembuatan profil tampak "normal" dan mudah dibaca:
Di sini kita menemukan 8,4 detik yang kita miliki dengan Stopwatch (membuat profil memperlambat perf). Kami juga menemukan HitCount = 3500 di sepanjang jalur panggilan, yang konsisten dengan 3500 baris dalam tes. Di sisi parser TDS, hal-hal mulai menjadi lebih buruk sejak kita membaca 118 353 panggilan pada TryReadByteArray()
metode, yang merupakan loop buffering terjadi. (rata-rata 33,8 panggilan untuk masing-masing byte[]
256kb)
Untuk async
kasus ini, ini benar-benar sangat berbeda .... Pertama, .ToListAsync()
panggilan dijadwalkan di ThreadPool, dan kemudian ditunggu. Tidak ada yang luar biasa di sini. Tapi, sekarang, inilah yang ada async
di ThreadPool:
Pertama, dalam kasus pertama kami hanya memiliki 3500 hitungan hit di sepanjang jalur panggilan penuh, di sini kami memiliki 118 371. Selain itu, Anda harus membayangkan semua panggilan sinkronisasi yang saya tidak lakukan pada screenshoot ...
Kedua, dalam kasus pertama, kami memiliki panggilan "hanya 118 353" untuk TryReadByteArray()
metode ini, di sini kami memiliki 2 050 210 panggilan! Ini 17 kali lebih banyak ... (pada tes dengan array 1Mb besar, 160 kali lebih banyak)
Apalagi ada:
- 120 000
Task
instance dibuat
- 727 519
Interlocked
panggilan
- 290 569
Monitor
panggilan
- 98 283
ExecutionContext
instance, dengan 264 481 Capture
- 208 733
SpinLock
panggilan
Dugaan saya adalah buffering dibuat dengan cara async (dan bukan yang bagus), dengan Tugas paralel mencoba membaca data dari TDS. Terlalu banyak tugas yang dibuat hanya untuk mem-parsing data biner.
Sebagai kesimpulan awal, kita dapat mengatakan Async hebat, EF6 hebat, tetapi penggunaan async EF6 dalam penerapannya saat ini menambahkan overhead yang besar, di sisi kinerja, sisi Threading, dan sisi CPU (penggunaan CPU 12% di ToList()
case dan 20% dalam ToListAsync
case untuk kerja 8 sampai 10 kali lebih lama ... Saya menjalankannya pada i7 920 lama).
Sambil melakukan beberapa tes, saya memikirkan artikel ini lagi dan saya perhatikan sesuatu yang saya lewatkan:
"Untuk metode asinkron baru di. Net 4.5, perilaku mereka persis sama dengan metode sinkron, kecuali untuk satu pengecualian penting: ReadAsync dalam mode non-sekuensial."
Apa ?!!!
Jadi saya memperluas tolok ukur saya untuk memasukkan Ado.Net dalam panggilan biasa / async, dan dengan CommandBehavior.SequentialAccess
/ CommandBehavior.Default
, dan inilah kejutan besar! :
Kami memiliki perilaku yang sama persis dengan Ado.Net !!! Telapak tangan...
Kesimpulan definitif saya adalah : ada bug dalam implementasi EF 6. Seharusnya beralih CommandBehavior
ke SequentialAccess
ketika panggilan async dilakukan atas tabel yang berisi binary(max)
kolom. Masalah membuat terlalu banyak Tugas, memperlambat proses, ada di sisi Ado.Net. Masalah EF adalah tidak menggunakan Ado.Net sebagaimana mestinya.
Sekarang Anda tahu daripada menggunakan metode async EF6, Anda lebih baik harus memanggil EF dengan cara non-async biasa, dan kemudian menggunakan a TaskCompletionSource<T>
untuk mengembalikan hasilnya dengan cara async.
Catatan 1: Saya mengedit posting saya karena kesalahan yang memalukan .... Saya telah melakukan tes pertama saya melalui jaringan, bukan secara lokal, dan bandwidth terbatas telah mengubah hasil. Berikut adalah hasil yang diperbarui.
Catatan 2: Saya tidak memperluas pengujian saya ke kasus kegunaan lain (mis: nvarchar(max)
dengan banyak data), tetapi ada kemungkinan perilaku yang sama terjadi.
Catatan 3: Sesuatu yang biasa untuk ToList()
kasus ini, adalah CPU 12% (1/8 dari CPU saya = 1 inti logis). Sesuatu yang tidak biasa adalah maksimum 20% untuk ToListAsync()
kasus ini, seolah-olah Penjadwal tidak dapat menggunakan semua Tapak. Itu mungkin karena terlalu banyak Tugas yang dibuat, atau mungkin hambatan dalam TDS parser, saya tidak tahu ...