ฉันพบคำถามนี้น่าสนใจมากโดยเฉพาะอย่างยิ่งเมื่อฉันใช้async
ทุกที่กับ Ado.Net และ EF 6 ฉันหวังว่าจะมีคนให้คำอธิบายสำหรับคำถามนี้ แต่ก็ไม่ได้เกิดขึ้น ดังนั้นฉันจึงพยายามทำซ้ำปัญหานี้ที่ด้านข้างของฉัน ฉันหวังว่าบางท่านจะพบว่าสิ่งนี้น่าสนใจ
ข่าวดีแรก: ฉันทำซ้ำ :) และความแตกต่างนั้นมหาศาล ด้วยปัจจัย 8 ...
ครั้งแรกที่ฉันสงสัยว่ามีบางอย่างเกี่ยวข้องกับCommandBehavior
เนื่องจากฉันอ่านบทความที่น่าสนใจเกี่ยวasync
กับ Ado พูดนี้:
"เนื่องจากโหมดการเข้าถึงที่ไม่ต่อเนื่องจำเป็นต้องเก็บข้อมูลสำหรับทั้งแถวจึงอาจทำให้เกิดปัญหาหากคุณอ่านคอลัมน์ขนาดใหญ่จากเซิร์ฟเวอร์ (เช่น varbinary (MAX), varchar (MAX), nvarchar (MAX) หรือ XML )."
ฉันสงสัยว่ามีการToList()
โทรเป็นCommandBehavior.SequentialAccess
และแบบอะซิงโครนัสเป็นแบบCommandBehavior.Default
ไม่ต่อเนื่องซึ่งอาจทำให้เกิดปัญหา ดังนั้นฉันจึงดาวน์โหลดแหล่งที่มาของ EF6 และใส่จุดพักทุกที่ ( CommandBehavior
แน่นอนว่าใช้ที่ไหน)
ส่งผลให้เกิด: ไม่มีอะไร ทุกการโทรทำด้วยCommandBehavior.Default
.... ดังนั้นฉันจึงพยายามที่จะก้าวไปสู่โค้ดของ EF เพื่อทำความเข้าใจว่าเกิดอะไรขึ้น ... และ .. ooouch ... ฉันไม่เคยเห็นรหัสผู้แทนเช่นนั้นทุกอย่างดูเหมือนจะถูกดำเนินการขี้เกียจ ...
ดังนั้นฉันจึงพยายามทำโปรไฟล์เพื่อทำความเข้าใจว่าเกิดอะไรขึ้น ...
และฉันคิดว่าฉันมีบางสิ่ง ...
นี่คือรูปแบบการสร้างตารางที่ฉันสร้างเกณฑ์มาตรฐานโดยมี 3,500 บรรทัดอยู่ในนั้นและ 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 วินาทีที่เรามีกับนาฬิกาจับเวลา (การทำโปรไฟล์ช้าลง perfs) นอกจากนี้เรายังพบ HitCount = 3500 ตามเส้นทางการโทรซึ่งสอดคล้องกับ 3500 สายในการทดสอบ ในด้านตัวแยกวิเคราะห์ TDS สิ่งต่าง ๆ เริ่มแย่ลงเนื่องจากเราอ่านTryReadByteArray()
วิธีการโทร 118 353 ซึ่งเกิดจากการวนรอบบัฟเฟอร์ (เฉลี่ย 33.8 สายสำหรับแต่ละbyte[]
256kb)
สำหรับasync
กรณีนี้มันแตกต่างกันจริงๆ .... ก่อนอื่นการ.ToListAsync()
โทรถูกกำหนดเวลาไว้ใน ThreadPool จากนั้นรอ ไม่มีอะไรน่าอัศจรรย์ที่นี่ แต่ตอนนี้นี่คือasync
นรกบน ThreadPool:
ครั้งแรกในกรณีแรกเรามีจำนวนการเข้าชม 3,500 ครั้งตลอดเส้นทางการโทรเต็มรูปแบบที่นี่เรามี 118 371 นอกจากนี้คุณต้องจินตนาการถึงการซิงโครไนซ์สายทั้งหมดที่ฉันไม่ได้วางบนหน้าจอ ...
ประการที่สองในกรณีแรกเราได้รับ "เพียง 118 353" TryReadByteArray()
วิธีการที่นี่เรามี 2 050 210 สาย! มันมากกว่า 17 เท่า ... (ในการทดสอบกับอาร์เรย์ 1Mb ขนาดใหญ่มันเพิ่มขึ้น 160 เท่า)
นอกจากนี้ยังมี:
Task
สร้างอินสแตนซ์120,000 รายการแล้ว
Interlocked
โทร727 519
- 290 569 การ
Monitor
โทร
ExecutionContext
อินสแตนซ์98 283 กับ 264 481 จับ
- การ
SpinLock
โทร208 733
ฉันเดาว่าการบัฟเฟอร์จะทำในลักษณะ async (และไม่ใช่แบบที่ดี) ด้วย Tasks แบบขนานที่พยายามอ่านข้อมูลจาก TDS มีการสร้าง Task มากเกินไปเพื่อแยกวิเคราะห์ข้อมูลไบนารี
จากบทสรุปเบื้องต้นเราสามารถพูดได้ว่า Async นั้นยอดเยี่ยม EF6 นั้นยอดเยี่ยม แต่การใช้ async ของ EF6 ในการใช้งานในปัจจุบันนั้นเพิ่มค่าใช้จ่ายที่สำคัญด้านประสิทธิภาพการทำงานด้าน Threading และด้าน CPU (การใช้งาน CPU 12% ในToList()
เคสและ 20% ในToListAsync
เคสสำหรับการทำงานนานกว่า 8 ถึง 10 เท่า ... ฉันรันบน i7 920 รุ่นเก่า)
ในขณะที่ทำการทดสอบบางอย่างฉันกำลังคิดถึงบทความนี้อีกครั้งและฉันสังเกตเห็นบางสิ่งที่ฉันพลาด:
"สำหรับวิธีการแบบอะซิงโครนัสใหม่ใน. Net 4.5 พฤติกรรมของพวกเขาเหมือนกับวิธีการแบบซิงโครนัสยกเว้นสำหรับข้อยกเว้นที่โดดเด่นอย่างหนึ่ง: ReadAsync ในโหมดที่ไม่ต่อเนื่อง"
อะไร ?!!!
ดังนั้นฉันจึงขยายมาตรฐานเพื่อรวม Ado.Net ในการโทรปกติ / async และด้วยCommandBehavior.SequentialAccess
/ CommandBehavior.Default
และนี่เป็นเรื่องที่น่าประหลาดใจมาก! :
เรามีพฤติกรรมเดียวกันกับ Ado.Net !!! facepalm ...
ข้อสรุปที่ชัดเจนของฉันคือมีข้อบกพร่องในการใช้งาน EF 6 มันควรจะสลับCommandBehavior
ไปSequentialAccess
เมื่อมีสายเรียก async จะทำมากกว่าตารางที่มีbinary(max)
คอลัมน์ ปัญหาของการสร้างงานมากเกินไปทำให้กระบวนการช้าลงคือด้าน Ado.Net ปัญหาของ EF คือไม่ใช้ Ado.Net เท่าที่ควร
ตอนนี้คุณรู้แทนที่จะใช้วิธี async ของ EF6 คุณจะต้องโทรหา EF ในแบบที่ไม่ใช่ async ตามปกติจากนั้นใช้ a TaskCompletionSource<T>
เพื่อส่งคืนผลลัพธ์ในรูปแบบ async
หมายเหตุ 1: ฉันแก้ไขโพสต์ของฉันเนื่องจากมีข้อผิดพลาดน่าอับอาย .... ฉันได้ทำการทดสอบครั้งแรกผ่านเครือข่ายไม่ใช่ในพื้นที่และแบนด์วิดท์ จำกัด มีการบิดเบือนผลลัพธ์ นี่คือผลการอัพเดท
หมายเหตุ 2: ฉันไม่ได้ขยายการทดสอบของฉันไปยังกรณีการใช้งานอื่น (เช่น: nvarchar(max)
มีข้อมูลจำนวนมาก) แต่มีโอกาสที่พฤติกรรมเดียวกันจะเกิดขึ้น
หมายเหตุ 3: สิ่งปกติสำหรับToList()
กรณีนี้คือ CPU 12% (1/8 ของ CPU = 1 ตรรกะหลัก) สิ่งผิดปกติคือสูงสุด 20% สำหรับToListAsync()
กรณีเช่นเดียวกับ Scheduler ไม่สามารถใช้ Treads ทั้งหมดได้ อาจเป็นเพราะงานที่สร้างขึ้นมากเกินไปหรืออาจเป็นปัญหาคอขวดในตัวแยกวิเคราะห์ TDS ฉันไม่รู้ ...