我发现这个问题非常有趣,尤其是因为我在async
Ado.Net和EF 6的任何地方都在使用它。我希望有人对此问题做出解释,但并没有发生。因此,我尝试重现此问题。我希望你们中的一些人会发现这很有趣。
第一个好消息:我转载了它:)区别是巨大的。因子8 ...
首先CommandBehavior
,由于我读了一篇有关async
Ado 的有趣文章,因此我怀疑与之相关的事情:
“由于非顺序访问模式必须存储整个行的数据,因此如果您正在从服务器读取大列(例如varbinary(MAX),varchar(MAX),nvarchar(MAX)或XML),则会导致问题)。”
我怀疑ToList()
呼叫为be 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,这里只有118371。此外,您还必须想象一下我没有在屏幕截图中看到的所有同步调用...
其次,在第一种情况下,我们仅对该TryReadByteArray()
方法有“ 118 353”个调用,这里有2050210个调用!是原来的17倍...(在具有1Mb大型阵列的测试中,它是原来的160倍)
此外,还有:
Task
创建了12 万个实例
- 727519
Interlocked
电话
- 290569
Monitor
电话
- 98283个
ExecutionContext
实例,其中包含264481个捕获
- 208733
SpinLock
电话
我的猜测是,缓冲是通过异步方式(而不是一种好的方式)进行的,并行任务试图从TDS读取数据。仅为了解析二进制数据,创建了太多任务。
作为初步结论,我们可以说Async很棒,EF6很棒,但是EF6在其当前实现中使用async会增加性能,线程和CPU方面的开销(在CPU方面为12%)。ToList()
情况下需要20%的ToListAsync
时间才能工作8到10倍...我在旧的i7 920上运行)。
在做一些测试时,我又在想这篇文章,我发现我想念的东西:
“对于.Net 4.5中的新异步方法,它们的行为与同步方法完全相同,除了一个显着的例外:非顺序模式下的ReadAsync。”
什么 ?!!!
因此,我将基准测试扩展到常规/异步调用中的Ado.Net,并通过CommandBehavior.SequentialAccess
/进行了扩展CommandBehavior.Default
,这是一个很大的惊喜!:
我们与Ado.Net具有完全相同的行为!Facepalm ...
我的明确结论是:EF 6实施中存在一个错误。当对包含列的表进行异步调用时,应将切换CommandBehavior
到。在Ado.Net方面,创建太多Task会减慢该过程的速度。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%是不寻常的情况,就好像调度程序无法使用所有的Treads。这可能是由于创建了太多任务,或者是TDS解析器中的瓶颈,我不知道...