Özellikle async
Ado.Net ve EF 6 ile her yerde kullandığım için bu soruyu çok ilginç buldum, birisinin bu soru için bir açıklama yapmasını umuyordum, ama olmadı. Bu sorunu kendi tarafımda yeniden oluşturmaya çalıştım. Umarım bazılarınız bunu ilginç bulur.
İlk iyi haber: Ben çoğalttım :) Ve fark çok büyük. Faktör 8 ile ...
İlk önce bir şeyle uğraştığından şüpheleniyordum CommandBehavior
beri ben ilginç bir yazı okudum hakkında async
bu söyleyerek, Ado ile:
"Sıralı olmayan erişim modu, tüm satırın verilerini saklamak zorunda olduğundan, sunucudan büyük bir sütun (varbinary (MAX), varchar (MAX), nvarchar (MAX) veya XML gibi) okuyorsanız sorunlara neden olabilir. )."
Şüpheleniyordum ToList()
CommandBehavior.SequentialAccess
ve async olanlar CommandBehavior.Default
( sorunlara neden olabilir) olmak için çağrı . Bu yüzden EF6'nın kaynaklarını indirdim ve her yere kesme noktaları koydum ( CommandBehavior
tabii ki nerede kullanılırsa).
Sonuç: hiçbir şey . Tüm aramalar yapılıyor CommandBehavior.Default
.... Bu yüzden neler olduğunu anlamak için EF koduna adım atmaya çalıştım ... ve .. ooouch ... Asla böyle bir temsilci kodu görmüyorum, her şey tembel görünüyor ...
Bu yüzden ne olduğunu anlamak için biraz profil yapmaya çalıştım ...
Sanırım bir şeyim var ...
İşte içinde 3500 satır ve her birinde 256 Kb rasgele veri bulunan, kıyasladığım tabloyu oluşturmak için model 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; }
}
İşte test verilerini oluşturmak için kullandığım kod ve EF'i karşılaştırmak.
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);
}
}
Normal EF çağrısı ( .ToList()
) için profil oluşturma "normal" görünür ve okunması kolaydır:
Burada Kronometre ile sahip olduğumuz 8.4 saniyeyi görüyoruz (perfs'de yavaşlama profili). Ayrıca test yolundaki 3500 hat ile tutarlı olan çağrı yolu boyunca HitCount = 3500 buluyoruz. TDS ayrıştırıcı tarafında, TryReadByteArray()
tamponlama döngüsünün meydana geldiği yöntemin 118 353 çağrısını okuduğumuz için işler daha da kötüleşmeye başladı . ( byte[]
256 kb'nin her biri için ortalama 33,8 çağrı )
İçin async
söz, o, Birinci .... gerçekten çok farklı .ToListAsync()
çağrı ThreadPool'da planlanan ve ardından beklenmektedir. Burada inanılmaz bir şey yok. Ama, şimdi, async
ThreadPool'daki cehennem:
Birincisi, ilk durumda tam çağrı yolu boyunca sadece 3500 isabet sayımı yapıyorduk, burada 118 371 var.
İkincisi, ilk durumda, TryReadByteArray()
metoda "sadece 118 353" çağrısı yapıyorduk , burada 2 050 210 çağrımız var! 17 kat daha fazla ... (büyük 1Mb dizisi ile yapılan bir testte 160 kat daha fazla)
Ayrıca:
- 120.000
Task
örnek oluşturuldu
- 727 519
Interlocked
çağrı
- 290569
Monitor
çağrı
- 98 283
ExecutionContext
bulut sunucusu, 264481 Yakalama ile
- 208733
SpinLock
çağrı
Tahminime göre, tamponlama, paralel Görevler TDS'den veri okumaya çalışırken, asenkron bir şekilde (iyi bir değil) yapılır. İkili verileri ayrıştırmak için çok fazla Görev oluşturulur.
Bir ön sonuç olarak, Async'in harika olduğunu, EF6'nın harika olduğunu söyleyebiliriz, ancak EF6'nın mevcut uygulamasında async kullanımları, performans tarafında, İş parçacığı tarafında ve CPU tarafında (% 12 CPU kullanımı) ToList()
durumda ve% 20ToListAsync
8 ila 10 kat daha fazla çalışma için ... ve eski i7 920 ile çalıştırıyorum).
Bazı testler yaparken, yine bu makaleyi düşünüyordum ve özlediğim bir şey fark ettim:
".Net 4.5'teki yeni eşzamansız yöntemler için, davranışları, dikkate değer bir istisna dışında, eşzamanlı yöntemlerle tamamen aynıdır: Sıralı olmayan modda ReadAsync."
Ne ?!!!
Ben Ado.Net düzenli / zaman uyumsuz çağrısında ve dahil etmek benim kriterler uzatmak Yani CommandBehavior.SequentialAccess
/ CommandBehavior.Default
ve burada büyük bir sürpriz! :
Ado.Net ile aynı davranışa sahibiz !!! Facepalm ...
Kesin sonuç : EF 6 uygulamasında bir hata var. Bu geçiş olmalıdır CommandBehavior
için SequentialAccess
bir zaman uyumsuz çağrı bir içeren bir tablo üzerinde yapıldığında binary(max)
sütunu. Çok fazla Görev yaratma, süreci yavaşlatma sorunu Ado.Net tarafında. EF sorunu Ado.Net'i gerektiği gibi kullanmamasıdır.
Artık EF6 zaman uyumsuz yöntemlerini kullanmak yerine EF'yi zaman uyumsuz bir şekilde çağırmanız ve ardından bir TaskCompletionSource<T>
sonucu zaman uyumsuz bir şekilde döndürmek için .
Not 1: Yazımı utanç verici bir hata nedeniyle düzenledim .... İlk testimi yerel olarak değil ağ üzerinden yaptım ve sınırlı bant genişliği sonuçları bozdu. İşte güncellenen sonuçlar.
Not 2: Testimi diğer kullanımlar için genişletmedim (ör: nvarchar(max)
çok fazla veri ile), ancak aynı davranışın gerçekleşme olasılığı vardır.
Not 3: ToList()
Durum için olağan bir şey ,% 12 CPU'dur (CPU'mun 1 / 8'i = 1 mantıksal çekirdek). ToListAsync()
Programlayıcı tüm Adımları kullanamıyormuş gibi olağandışı bir şey, vaka için maksimum% 20'dir . Muhtemelen çok fazla Görev yaratıldı, ya da belki TDS ayrıştırıcısında bir darboğaz, bilmiyorum ...