Entity Framework 异步操作需要十倍的时间才能完成
Entity Framework async operation takes ten times as long to complete
我有一个使用 Entity Framework 6 处理数据库的 MVC 站点,我一直在尝试更改它,以便所有 运行 都作为异步控制器并调用数据库是 运行 作为它们的异步对应物(例如 ToListAsync() 而不是 ToList())
我遇到的问题是简单地将我的查询更改为异步导致它们非常慢。
以下代码从我的数据上下文中获取 "Album" 个对象的集合,并且 运行 将用于相当简单的数据库连接:
// Get the albums
var albums = await this.context.Albums
.Where(x => x.Artist.ID == artist.ID)
.ToListAsync();
这是创建的 SQL:
exec sp_executesql N'SELECT
[Extent1].[ID] AS [ID],
[Extent1].[URL] AS [URL],
[Extent1].[ASIN] AS [ASIN],
[Extent1].[Title] AS [Title],
[Extent1].[ReleaseDate] AS [ReleaseDate],
[Extent1].[AccurateDay] AS [AccurateDay],
[Extent1].[AccurateMonth] AS [AccurateMonth],
[Extent1].[Type] AS [Type],
[Extent1].[Tracks] AS [Tracks],
[Extent1].[MainCredits] AS [MainCredits],
[Extent1].[SupportingCredits] AS [SupportingCredits],
[Extent1].[Description] AS [Description],
[Extent1].[Image] AS [Image],
[Extent1].[HasImage] AS [HasImage],
[Extent1].[Created] AS [Created],
[Extent1].[Artist_ID] AS [Artist_ID]
FROM [dbo].[Albums] AS [Extent1]
WHERE [Extent1].[Artist_ID] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=134
事实上,这不是一个非常复杂的查询,但是 SQL 服务器 运行 它花费了将近 6 秒。 SQL Server Profiler 报告它需要 5742 毫秒才能完成。
如果我将代码更改为:
// Get the albums
var albums = this.context.Albums
.Where(x => x.Artist.ID == artist.ID)
.ToList();
然后生成完全相同的 SQL,但是根据 SQL Server Profiler,这个 运行 只用了 474 毫秒。
数据库在 "Albums" table 中有大约 3500 行,这并不是很多,并且在 "Artist_ID" 列上有一个索引,所以它应该很漂亮快.
我知道异步有开销,但让事情慢十倍对我来说似乎有点陡峭!我哪里错了?
我发现这个问题非常有趣,尤其是因为我在 Ado.Net 和 EF 6 中到处都使用 async
。我希望有人能对这个问题给出解释,但事实并非如此它发生了。所以我试图在我这边重现这个问题。我希望你们中的一些人会觉得这很有趣。
第一个好消息:我转载了它:)而且差别很大。因子 8 ...
首先我怀疑是关于 CommandBehavior
, since I read an interesting article 关于 async
和 Ado 的事情,这样说:
"Since non-sequential access mode has to store the data for the entire row, it can cause issues if you are reading a large column from the server (such as varbinary(MAX), varchar(MAX), nvarchar(MAX) or XML)."
我怀疑 ToList()
调用是 CommandBehavior.SequentialAccess
而异步调用是 CommandBehavior.Default
(非顺序的,这可能会导致问题)。因此,我下载了 EF6 的源代码,并在各处设置了断点(当然,使用 CommandBehavior
的地方)。
结果:无。所有调用都是使用 CommandBehavior.Default
进行的……所以我尝试进入 EF 代码以了解发生了什么……而且……糟糕……我从未见过这样的委托代码,一切似乎都是懒惰执行的。 ..
所以我尝试做一些分析以了解发生了什么...
而且我想我有一些东西...
这是创建我进行基准测试的 table 的模型,其中有 3500 行,每个 varbinary(MAX)
中有 256 Kb 随机数据。 (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()
),分析似乎 "normal" 并且易于阅读:
在这里我们找到了秒表的 8.4 秒(性能分析减慢了性能)。我们在调用路径上也发现HitCount = 3500,这与测试中的3500行是一致的。在 TDS 解析器端,事情开始变得更糟,因为我们读取了 118 353 次对 TryReadByteArray()
方法的调用,这是缓冲循环发生的地方。 (每个 byte[]
的 256kb 平均调用 33.8 次)
对于 async
的情况,它真的很不一样....首先,.ToListAsync()
调用被安排在 ThreadPool 上,然后等待。这里没什么了不起的。但是,现在,这里是 ThreadPool 上的 async
地狱:
首先,在第一种情况下,我们在整个调用路径上只有 3500 次点击计数,这里我们有 118 371 次。此外,您必须想象我没有放在屏幕截图上的所有同步调用。 .
其次,在第一种情况下,我们对 TryReadByteArray()
方法进行了 "just 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 读取数据。创建了太多的任务只是为了解析二进制数据。
作为初步结论,我们可以说 Async 很棒,EF6 很棒,但是 EF6 在其当前实现中对异步的使用在性能方面、线程方面和 CPU 方面(在 ToList()
情况下使用 12% CPU,在 ToListAsync
情况下使用 20%,工作时间延长 8 到 10 倍...我 运行 它在旧 i7 920)。
在做一些测试时,我在想 this article again,但我注意到我错过了什么:
"For the new asynchronous methods in .Net 4.5, their behavior is exactly the same as with the synchronous methods, except for one notable exception: ReadAsync in non-sequential mode."
什么?!!!
所以我扩展了我的基准测试以在常规/异步调用中包含 Ado.Net,并且包含 CommandBehavior.SequentialAccess
/ CommandBehavior.Default
,这是一个很大的惊喜! :
我们的行为与 Ado.Net 完全相同!捂脸...
我的最终结论是:EF 6 实现中存在错误。当对包含 binary(max)
列的 table 进行异步调用时,它应该将 CommandBehavior
切换为 SequentialAccess
。创建太多 Task 导致进程变慢的问题在 Ado.Net 方面。 EF 问题是它没有按应有的方式使用 Ado.Net。
现在您知道了,与其使用 EF6 异步方法,不如以常规非异步方式调用 EF,然后使用 TaskCompletionSource<T>
到 return 结果异步方式。
注意 1:由于可耻的错误,我编辑了我的 post...。我已经通过网络而不是本地完成了我的第一次测试,有限的带宽扭曲了结果。这是更新后的结果。
注意 2:我没有将我的测试扩展到其他用例(例如:nvarchar(max)
有大量数据),但有可能发生相同的行为。
注 3:ToList()
情况下的常见情况是 12% CPU(我的 CPU = 1 个逻辑核心的 1/8)。 ToListAsync()
情况下的最大 20% 是不寻常的,就好像调度程序无法使用所有 Treads 一样。可能是创建的Task太多了,也可能是TDS解析器的瓶颈,不知道...
因为我几天前 link 收到了这个问题的回复,所以我决定 post 进行一个小的更新。我能够使用当前最新版本的 EF (6.4.0) 和 .NET Framework 4.7.2 重现 的结果。令人惊讶的是,这个问题从未得到改善。
.NET Framework 4.7.2 | EF 6.4.0 (Values in ms. Average of 10 runs)
non async : 3016
async : 20415
ExecuteReaderAsync SequentialAccess : 2780
ExecuteReaderAsync Default : 21061
ExecuteReader SequentialAccess : 3467
ExecuteReader Default : 3074
这引出了一个问题:dotnet core 有改进吗?
我将原始答案中的代码复制到新的 dotnet core 3.1.3 项目并添加了 EF Core 3.1.3。结果是:
dotnet core 3.1.3 | EF Core 3.1.3 (Values in ms. Average of 10 runs)
non async : 2780
async : 6563
ExecuteReaderAsync SequentialAccess : 2593
ExecuteReaderAsync Default : 6679
ExecuteReader SequentialAccess : 2668
ExecuteReader Default : 2315
令人惊讶的是有很多改进。似乎仍然存在一些时间延迟,因为线程池被调用,但它比 .NET Framework 实现快大约 3 倍。
我希望这个回答能帮助到以后以这种方式发送的其他人。
有一个解决方案允许在不牺牲性能的情况下使用异步,已使用 EF Core 和 MS SQL 数据库进行测试。
首先,您需要为 DBDataReader
:
制作一个包装器
- 它的
ReadAsync
方法应该读取整行,将每一列的值存储在缓冲区中。
- 它的
GetXyz
方法应该从上述缓冲区中获取值。
- 可选地,使用
GetBytes
+ Encoding.GetString
而不是 GetString
。对于我的用例(每行 16KB 文本列),它显着提高了同步和异步的速度。
- 可以选择调整连接字符串的数据包大小。对于我的用例,值 32767 导致同步和异步的显着加速。
您现在可以制作一个 DbCommandInterceptor
,拦截 ReaderExecutingAsync
以创建一个具有顺序访问的 DBDataReader
,由上述包装器包装。
EF Core 将尝试以非顺序方式访问字段 - 这就是包装器必须首先读取和缓冲整行的原因。
这是一个示例实现(拦截异步和同步):
/// <summary>
/// This interceptor optimizes a <see cref="Microsoft.EntityFrameworkCore.DbContext"/> for
/// accessing large columns (text, ntext, varchar(max) and nvarchar(max)). It enables the
/// <see cref="CommandBehavior.SequentialAccess"/> option and uses an optimized method
/// for converting large text columns into <see cref="string"/> objects.
/// </summary>
public class ExampleDbCommandInterceptor : DbCommandInterceptor
{
public async override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
{
var behavior = CommandBehavior.SequentialAccess;
var reader = await command.ExecuteReaderAsync(behavior, cancellationToken).ConfigureAwait(false);
var wrapper = await DbDataReaderOptimizedWrapper.CreateAsync(reader, cancellationToken).ConfigureAwait(false);
return InterceptionResult<DbDataReader>.SuppressWithResult(wrapper);
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
var behavior = CommandBehavior.SequentialAccess;
var reader = command.ExecuteReader(behavior);
var wrapper = DbDataReaderOptimizedWrapper.Create(reader);
return InterceptionResult<DbDataReader>.SuppressWithResult(wrapper);
}
/// <summary>
/// This wrapper caches the values of accessed columns of each row, allowing non-sequential access
/// even when <see cref="CommandBehavior.SequentialAccess"/> is specified. It enables using this option it with EF Core.
/// In addition, it provides an optimized method for reading text, ntext, varchar(max) and nvarchar(max) columns.
/// All in all, it speeds up database operations reading from large text columns.
/// </summary>
sealed class DbDataReaderOptimizedWrapper : DbDataReader
{
readonly DbDataReader reader;
readonly DbColumn[] schema;
readonly object[] cache;
readonly Func<object>[] materializers;
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private T Get<T>(int ordinal)
{
if (cache[ordinal] != DBNull.Value) return (T)cache[ordinal];
return (T)(object)null; // this line will throw an exception if T is not a reference type (class), otherwise it will return null
}
private DbDataReaderOptimizedWrapper(DbDataReader reader, IEnumerable<DbColumn> schema)
{
this.reader = reader;
this.schema = schema.OrderBy(x => x.ColumnOrdinal).ToArray();
cache = new object[this.schema.Length];
byte[] stringGetterBuffer = null;
string stringGetter(int i)
{
var dbColumn = this.schema[i];
// Using GetBytes instead of GetString is much faster, but only works for text, ntext, varchar(max) and nvarchar(max)
if (dbColumn.ColumnSize < int.MaxValue) return reader.GetString(i);
if (stringGetterBuffer == null) stringGetterBuffer = new byte[32 * 1024];
var totalRead = 0;
while (true)
{
var offset = totalRead;
totalRead += (int)reader.GetBytes(i, offset, stringGetterBuffer, offset, stringGetterBuffer.Length - offset);
if (totalRead < stringGetterBuffer.Length) break;
const int maxBufferSize = int.MaxValue / 2;
if (stringGetterBuffer.Length >= maxBufferSize)
throw new OutOfMemoryException($"{nameof(DbDataReaderOptimizedWrapper)}.{nameof(GetString)} cannot load column '{GetName(i)}' because it contains a string longer than {maxBufferSize} bytes.");
Array.Resize(ref stringGetterBuffer, 2 * stringGetterBuffer.Length);
}
var c = dbColumn.DataTypeName[0];
var encoding = (c is 'N' or 'n') ? Encoding.Unicode : Encoding.ASCII;
return encoding.GetString(stringGetterBuffer.AsSpan(0, totalRead));
}
var dict = new Dictionary<Type, Func<DbColumn, int, Func<object>>>
{
[typeof(bool)] = (column, index) => () => reader.GetBoolean(index),
[typeof(byte)] = (column, index) => () => reader.GetByte(index),
[typeof(char)] = (column, index) => () => reader.GetChar(index),
[typeof(short)] = (column, index) => () => reader.GetInt16(index),
[typeof(int)] = (column, index) => () => reader.GetInt32(index),
[typeof(long)] = (column, index) => () => reader.GetInt64(index),
[typeof(float)] = (column, index) => () => reader.GetFloat(index),
[typeof(double)] = (column, index) => () => reader.GetDouble(index),
[typeof(decimal)] = (column, index) => () => reader.GetDecimal(index),
[typeof(DateTime)] = (column, index) => () => reader.GetDateTime(index),
[typeof(Guid)] = (column, index) => () => reader.GetGuid(index),
[typeof(string)] = (column, index) => () => stringGetter(index),
};
materializers = schema.Select((column, index) => dict[column.DataType](column, index)).ToArray();
}
public static DbDataReaderOptimizedWrapper Create(DbDataReader reader)
=> new DbDataReaderOptimizedWrapper(reader, reader.GetColumnSchema());
public static async ValueTask<DbDataReaderOptimizedWrapper> CreateAsync(DbDataReader reader, CancellationToken cancellationToken)
=> new DbDataReaderOptimizedWrapper(reader, await reader.GetColumnSchemaAsync(cancellationToken).ConfigureAwait(false));
protected override void Dispose(bool disposing) => reader.Dispose();
public async override ValueTask DisposeAsync() => await reader.DisposeAsync().ConfigureAwait(false);
public override object this[int ordinal] => Get<object>(ordinal);
public override object this[string name] => Get<object>(GetOrdinal(name));
public override int Depth => reader.Depth;
public override int FieldCount => reader.FieldCount;
public override bool HasRows => reader.HasRows;
public override bool IsClosed => reader.IsClosed;
public override int RecordsAffected => reader.RecordsAffected;
public override int VisibleFieldCount => reader.VisibleFieldCount;
public override bool GetBoolean(int ordinal) => Get<bool>(ordinal);
public override byte GetByte(int ordinal) => Get<byte>(ordinal);
public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) => throw new NotSupportedException();
public override char GetChar(int ordinal) => Get<char>(ordinal);
public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) => throw new NotSupportedException();
public override string GetDataTypeName(int ordinal) => reader.GetDataTypeName(ordinal);
public override DateTime GetDateTime(int ordinal) => Get<DateTime>(ordinal);
public override decimal GetDecimal(int ordinal) => Get<decimal>(ordinal);
public override double GetDouble(int ordinal) => Get<double>(ordinal);
public override IEnumerator GetEnumerator() => reader.GetEnumerator();
public override Type GetFieldType(int ordinal) => reader.GetFieldType(ordinal);
public override float GetFloat(int ordinal) => Get<float>(ordinal);
public override Guid GetGuid(int ordinal) => Get<Guid>(ordinal);
public override short GetInt16(int ordinal) => Get<short>(ordinal);
public override int GetInt32(int ordinal) => Get<int>(ordinal);
public override long GetInt64(int ordinal) => Get<long>(ordinal);
public override string GetName(int ordinal) => reader.GetName(ordinal);
public override int GetOrdinal(string name) => reader.GetOrdinal(name);
public override string GetString(int ordinal) => Get<string>(ordinal);
public override object GetValue(int ordinal) => Get<object>(ordinal);
public override int GetValues(object[] values)
{
var min = Math.Min(cache.Length, values.Length);
Array.Copy(cache, values, min);
return min;
}
public override bool IsDBNull(int ordinal) => Convert.IsDBNull(cache[ordinal]);
public override bool NextResult() => reader.NextResult();
public override bool Read()
{
Array.Clear(cache, 0, cache.Length);
if (reader.Read())
{
for (int i = 0; i < cache.Length; ++i)
{
if ((schema[i].AllowDBNull ?? true) && reader.IsDBNull(i))
cache[i] = DBNull.Value;
else cache[i] = materializers[i]();
}
return true;
}
return false;
}
public override void Close() => reader.Close();
public async override Task CloseAsync() => await reader.CloseAsync().ConfigureAwait(false);
public override DataTable GetSchemaTable() => reader.GetSchemaTable();
public async override Task<DataTable> GetSchemaTableAsync(CancellationToken cancellationToken = default) => await reader.GetSchemaTableAsync(cancellationToken).ConfigureAwait(false);
public async override Task<ReadOnlyCollection<DbColumn>> GetColumnSchemaAsync(CancellationToken cancellationToken = default) => await reader.GetColumnSchemaAsync(cancellationToken).ConfigureAwait(false);
public async override Task<bool> NextResultAsync(CancellationToken cancellationToken) => await reader.NextResultAsync(cancellationToken).ConfigureAwait(false);
public async override Task<bool> ReadAsync(CancellationToken cancellationToken)
{
Array.Clear(cache, 0, cache.Length);
if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
for (int i = 0; i < cache.Length; ++i)
{
if ((schema[i].AllowDBNull ?? true) && await reader.IsDBNullAsync(i, cancellationToken).ConfigureAwait(false))
cache[i] = DBNull.Value;
else cache[i] = materializers[i]();
}
return true;
}
return false;
}
}
}
我现在无法提供基准,希望有人能在评论中提供。
添加到@rducom 给出的答案中。 Microsoft.EntityFrameworkCore 6.0.0
中仍然存在此问题
阻塞部分实际上是 SqlClient
,@AndriySvyryd 推荐的适用于 EF 核心项目的解决方法是:
Don't use VARCHAR(MAX) or don't use async queries.
我在使用 async
查询读取大型 JSON 对象和图像(二进制)数据时发生了这种情况。
链接:
https://github.com/dotnet/efcore/issues/18571#issuecomment-545992812
https://github.com/dotnet/efcore/issues/18571
https://github.com/dotnet/efcore/issues/885
我的快速修复方法是将调用包装在一个任务中,而只使用同步方法。
这不是一个通用的解决方案,但对于小的聚合,它可以限制在应用程序的一小部分。
我有一个使用 Entity Framework 6 处理数据库的 MVC 站点,我一直在尝试更改它,以便所有 运行 都作为异步控制器并调用数据库是 运行 作为它们的异步对应物(例如 ToListAsync() 而不是 ToList())
我遇到的问题是简单地将我的查询更改为异步导致它们非常慢。
以下代码从我的数据上下文中获取 "Album" 个对象的集合,并且 运行 将用于相当简单的数据库连接:
// Get the albums
var albums = await this.context.Albums
.Where(x => x.Artist.ID == artist.ID)
.ToListAsync();
这是创建的 SQL:
exec sp_executesql N'SELECT
[Extent1].[ID] AS [ID],
[Extent1].[URL] AS [URL],
[Extent1].[ASIN] AS [ASIN],
[Extent1].[Title] AS [Title],
[Extent1].[ReleaseDate] AS [ReleaseDate],
[Extent1].[AccurateDay] AS [AccurateDay],
[Extent1].[AccurateMonth] AS [AccurateMonth],
[Extent1].[Type] AS [Type],
[Extent1].[Tracks] AS [Tracks],
[Extent1].[MainCredits] AS [MainCredits],
[Extent1].[SupportingCredits] AS [SupportingCredits],
[Extent1].[Description] AS [Description],
[Extent1].[Image] AS [Image],
[Extent1].[HasImage] AS [HasImage],
[Extent1].[Created] AS [Created],
[Extent1].[Artist_ID] AS [Artist_ID]
FROM [dbo].[Albums] AS [Extent1]
WHERE [Extent1].[Artist_ID] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=134
事实上,这不是一个非常复杂的查询,但是 SQL 服务器 运行 它花费了将近 6 秒。 SQL Server Profiler 报告它需要 5742 毫秒才能完成。
如果我将代码更改为:
// Get the albums
var albums = this.context.Albums
.Where(x => x.Artist.ID == artist.ID)
.ToList();
然后生成完全相同的 SQL,但是根据 SQL Server Profiler,这个 运行 只用了 474 毫秒。
数据库在 "Albums" table 中有大约 3500 行,这并不是很多,并且在 "Artist_ID" 列上有一个索引,所以它应该很漂亮快.
我知道异步有开销,但让事情慢十倍对我来说似乎有点陡峭!我哪里错了?
我发现这个问题非常有趣,尤其是因为我在 Ado.Net 和 EF 6 中到处都使用 async
。我希望有人能对这个问题给出解释,但事实并非如此它发生了。所以我试图在我这边重现这个问题。我希望你们中的一些人会觉得这很有趣。
第一个好消息:我转载了它:)而且差别很大。因子 8 ...
首先我怀疑是关于 CommandBehavior
, since I read an interesting article 关于 async
和 Ado 的事情,这样说:
"Since non-sequential access mode has to store the data for the entire row, it can cause issues if you are reading a large column from the server (such as varbinary(MAX), varchar(MAX), nvarchar(MAX) or XML)."
我怀疑 ToList()
调用是 CommandBehavior.SequentialAccess
而异步调用是 CommandBehavior.Default
(非顺序的,这可能会导致问题)。因此,我下载了 EF6 的源代码,并在各处设置了断点(当然,使用 CommandBehavior
的地方)。
结果:无。所有调用都是使用 CommandBehavior.Default
进行的……所以我尝试进入 EF 代码以了解发生了什么……而且……糟糕……我从未见过这样的委托代码,一切似乎都是懒惰执行的。 ..
所以我尝试做一些分析以了解发生了什么...
而且我想我有一些东西...
这是创建我进行基准测试的 table 的模型,其中有 3500 行,每个 varbinary(MAX)
中有 256 Kb 随机数据。 (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()
),分析似乎 "normal" 并且易于阅读:
在这里我们找到了秒表的 8.4 秒(性能分析减慢了性能)。我们在调用路径上也发现HitCount = 3500,这与测试中的3500行是一致的。在 TDS 解析器端,事情开始变得更糟,因为我们读取了 118 353 次对 TryReadByteArray()
方法的调用,这是缓冲循环发生的地方。 (每个 byte[]
的 256kb 平均调用 33.8 次)
对于 async
的情况,它真的很不一样....首先,.ToListAsync()
调用被安排在 ThreadPool 上,然后等待。这里没什么了不起的。但是,现在,这里是 ThreadPool 上的 async
地狱:
首先,在第一种情况下,我们在整个调用路径上只有 3500 次点击计数,这里我们有 118 371 次。此外,您必须想象我没有放在屏幕截图上的所有同步调用。 .
其次,在第一种情况下,我们对 TryReadByteArray()
方法进行了 "just 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 读取数据。创建了太多的任务只是为了解析二进制数据。
作为初步结论,我们可以说 Async 很棒,EF6 很棒,但是 EF6 在其当前实现中对异步的使用在性能方面、线程方面和 CPU 方面(在 ToList()
情况下使用 12% CPU,在 ToListAsync
情况下使用 20%,工作时间延长 8 到 10 倍...我 运行 它在旧 i7 920)。
在做一些测试时,我在想 this article again,但我注意到我错过了什么:
"For the new asynchronous methods in .Net 4.5, their behavior is exactly the same as with the synchronous methods, except for one notable exception: ReadAsync in non-sequential mode."
什么?!!!
所以我扩展了我的基准测试以在常规/异步调用中包含 Ado.Net,并且包含 CommandBehavior.SequentialAccess
/ CommandBehavior.Default
,这是一个很大的惊喜! :
我们的行为与 Ado.Net 完全相同!捂脸...
我的最终结论是:EF 6 实现中存在错误。当对包含 binary(max)
列的 table 进行异步调用时,它应该将 CommandBehavior
切换为 SequentialAccess
。创建太多 Task 导致进程变慢的问题在 Ado.Net 方面。 EF 问题是它没有按应有的方式使用 Ado.Net。
现在您知道了,与其使用 EF6 异步方法,不如以常规非异步方式调用 EF,然后使用 TaskCompletionSource<T>
到 return 结果异步方式。
注意 1:由于可耻的错误,我编辑了我的 post...。我已经通过网络而不是本地完成了我的第一次测试,有限的带宽扭曲了结果。这是更新后的结果。
注意 2:我没有将我的测试扩展到其他用例(例如:nvarchar(max)
有大量数据),但有可能发生相同的行为。
注 3:ToList()
情况下的常见情况是 12% CPU(我的 CPU = 1 个逻辑核心的 1/8)。 ToListAsync()
情况下的最大 20% 是不寻常的,就好像调度程序无法使用所有 Treads 一样。可能是创建的Task太多了,也可能是TDS解析器的瓶颈,不知道...
因为我几天前 link 收到了这个问题的回复,所以我决定 post 进行一个小的更新。我能够使用当前最新版本的 EF (6.4.0) 和 .NET Framework 4.7.2 重现
.NET Framework 4.7.2 | EF 6.4.0 (Values in ms. Average of 10 runs)
non async : 3016
async : 20415
ExecuteReaderAsync SequentialAccess : 2780
ExecuteReaderAsync Default : 21061
ExecuteReader SequentialAccess : 3467
ExecuteReader Default : 3074
这引出了一个问题:dotnet core 有改进吗?
我将原始答案中的代码复制到新的 dotnet core 3.1.3 项目并添加了 EF Core 3.1.3。结果是:
dotnet core 3.1.3 | EF Core 3.1.3 (Values in ms. Average of 10 runs)
non async : 2780
async : 6563
ExecuteReaderAsync SequentialAccess : 2593
ExecuteReaderAsync Default : 6679
ExecuteReader SequentialAccess : 2668
ExecuteReader Default : 2315
令人惊讶的是有很多改进。似乎仍然存在一些时间延迟,因为线程池被调用,但它比 .NET Framework 实现快大约 3 倍。
我希望这个回答能帮助到以后以这种方式发送的其他人。
有一个解决方案允许在不牺牲性能的情况下使用异步,已使用 EF Core 和 MS SQL 数据库进行测试。
首先,您需要为 DBDataReader
:
- 它的
ReadAsync
方法应该读取整行,将每一列的值存储在缓冲区中。 - 它的
GetXyz
方法应该从上述缓冲区中获取值。 - 可选地,使用
GetBytes
+Encoding.GetString
而不是GetString
。对于我的用例(每行 16KB 文本列),它显着提高了同步和异步的速度。 - 可以选择调整连接字符串的数据包大小。对于我的用例,值 32767 导致同步和异步的显着加速。
您现在可以制作一个 DbCommandInterceptor
,拦截 ReaderExecutingAsync
以创建一个具有顺序访问的 DBDataReader
,由上述包装器包装。
EF Core 将尝试以非顺序方式访问字段 - 这就是包装器必须首先读取和缓冲整行的原因。
这是一个示例实现(拦截异步和同步):
/// <summary>
/// This interceptor optimizes a <see cref="Microsoft.EntityFrameworkCore.DbContext"/> for
/// accessing large columns (text, ntext, varchar(max) and nvarchar(max)). It enables the
/// <see cref="CommandBehavior.SequentialAccess"/> option and uses an optimized method
/// for converting large text columns into <see cref="string"/> objects.
/// </summary>
public class ExampleDbCommandInterceptor : DbCommandInterceptor
{
public async override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
{
var behavior = CommandBehavior.SequentialAccess;
var reader = await command.ExecuteReaderAsync(behavior, cancellationToken).ConfigureAwait(false);
var wrapper = await DbDataReaderOptimizedWrapper.CreateAsync(reader, cancellationToken).ConfigureAwait(false);
return InterceptionResult<DbDataReader>.SuppressWithResult(wrapper);
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
var behavior = CommandBehavior.SequentialAccess;
var reader = command.ExecuteReader(behavior);
var wrapper = DbDataReaderOptimizedWrapper.Create(reader);
return InterceptionResult<DbDataReader>.SuppressWithResult(wrapper);
}
/// <summary>
/// This wrapper caches the values of accessed columns of each row, allowing non-sequential access
/// even when <see cref="CommandBehavior.SequentialAccess"/> is specified. It enables using this option it with EF Core.
/// In addition, it provides an optimized method for reading text, ntext, varchar(max) and nvarchar(max) columns.
/// All in all, it speeds up database operations reading from large text columns.
/// </summary>
sealed class DbDataReaderOptimizedWrapper : DbDataReader
{
readonly DbDataReader reader;
readonly DbColumn[] schema;
readonly object[] cache;
readonly Func<object>[] materializers;
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private T Get<T>(int ordinal)
{
if (cache[ordinal] != DBNull.Value) return (T)cache[ordinal];
return (T)(object)null; // this line will throw an exception if T is not a reference type (class), otherwise it will return null
}
private DbDataReaderOptimizedWrapper(DbDataReader reader, IEnumerable<DbColumn> schema)
{
this.reader = reader;
this.schema = schema.OrderBy(x => x.ColumnOrdinal).ToArray();
cache = new object[this.schema.Length];
byte[] stringGetterBuffer = null;
string stringGetter(int i)
{
var dbColumn = this.schema[i];
// Using GetBytes instead of GetString is much faster, but only works for text, ntext, varchar(max) and nvarchar(max)
if (dbColumn.ColumnSize < int.MaxValue) return reader.GetString(i);
if (stringGetterBuffer == null) stringGetterBuffer = new byte[32 * 1024];
var totalRead = 0;
while (true)
{
var offset = totalRead;
totalRead += (int)reader.GetBytes(i, offset, stringGetterBuffer, offset, stringGetterBuffer.Length - offset);
if (totalRead < stringGetterBuffer.Length) break;
const int maxBufferSize = int.MaxValue / 2;
if (stringGetterBuffer.Length >= maxBufferSize)
throw new OutOfMemoryException($"{nameof(DbDataReaderOptimizedWrapper)}.{nameof(GetString)} cannot load column '{GetName(i)}' because it contains a string longer than {maxBufferSize} bytes.");
Array.Resize(ref stringGetterBuffer, 2 * stringGetterBuffer.Length);
}
var c = dbColumn.DataTypeName[0];
var encoding = (c is 'N' or 'n') ? Encoding.Unicode : Encoding.ASCII;
return encoding.GetString(stringGetterBuffer.AsSpan(0, totalRead));
}
var dict = new Dictionary<Type, Func<DbColumn, int, Func<object>>>
{
[typeof(bool)] = (column, index) => () => reader.GetBoolean(index),
[typeof(byte)] = (column, index) => () => reader.GetByte(index),
[typeof(char)] = (column, index) => () => reader.GetChar(index),
[typeof(short)] = (column, index) => () => reader.GetInt16(index),
[typeof(int)] = (column, index) => () => reader.GetInt32(index),
[typeof(long)] = (column, index) => () => reader.GetInt64(index),
[typeof(float)] = (column, index) => () => reader.GetFloat(index),
[typeof(double)] = (column, index) => () => reader.GetDouble(index),
[typeof(decimal)] = (column, index) => () => reader.GetDecimal(index),
[typeof(DateTime)] = (column, index) => () => reader.GetDateTime(index),
[typeof(Guid)] = (column, index) => () => reader.GetGuid(index),
[typeof(string)] = (column, index) => () => stringGetter(index),
};
materializers = schema.Select((column, index) => dict[column.DataType](column, index)).ToArray();
}
public static DbDataReaderOptimizedWrapper Create(DbDataReader reader)
=> new DbDataReaderOptimizedWrapper(reader, reader.GetColumnSchema());
public static async ValueTask<DbDataReaderOptimizedWrapper> CreateAsync(DbDataReader reader, CancellationToken cancellationToken)
=> new DbDataReaderOptimizedWrapper(reader, await reader.GetColumnSchemaAsync(cancellationToken).ConfigureAwait(false));
protected override void Dispose(bool disposing) => reader.Dispose();
public async override ValueTask DisposeAsync() => await reader.DisposeAsync().ConfigureAwait(false);
public override object this[int ordinal] => Get<object>(ordinal);
public override object this[string name] => Get<object>(GetOrdinal(name));
public override int Depth => reader.Depth;
public override int FieldCount => reader.FieldCount;
public override bool HasRows => reader.HasRows;
public override bool IsClosed => reader.IsClosed;
public override int RecordsAffected => reader.RecordsAffected;
public override int VisibleFieldCount => reader.VisibleFieldCount;
public override bool GetBoolean(int ordinal) => Get<bool>(ordinal);
public override byte GetByte(int ordinal) => Get<byte>(ordinal);
public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) => throw new NotSupportedException();
public override char GetChar(int ordinal) => Get<char>(ordinal);
public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) => throw new NotSupportedException();
public override string GetDataTypeName(int ordinal) => reader.GetDataTypeName(ordinal);
public override DateTime GetDateTime(int ordinal) => Get<DateTime>(ordinal);
public override decimal GetDecimal(int ordinal) => Get<decimal>(ordinal);
public override double GetDouble(int ordinal) => Get<double>(ordinal);
public override IEnumerator GetEnumerator() => reader.GetEnumerator();
public override Type GetFieldType(int ordinal) => reader.GetFieldType(ordinal);
public override float GetFloat(int ordinal) => Get<float>(ordinal);
public override Guid GetGuid(int ordinal) => Get<Guid>(ordinal);
public override short GetInt16(int ordinal) => Get<short>(ordinal);
public override int GetInt32(int ordinal) => Get<int>(ordinal);
public override long GetInt64(int ordinal) => Get<long>(ordinal);
public override string GetName(int ordinal) => reader.GetName(ordinal);
public override int GetOrdinal(string name) => reader.GetOrdinal(name);
public override string GetString(int ordinal) => Get<string>(ordinal);
public override object GetValue(int ordinal) => Get<object>(ordinal);
public override int GetValues(object[] values)
{
var min = Math.Min(cache.Length, values.Length);
Array.Copy(cache, values, min);
return min;
}
public override bool IsDBNull(int ordinal) => Convert.IsDBNull(cache[ordinal]);
public override bool NextResult() => reader.NextResult();
public override bool Read()
{
Array.Clear(cache, 0, cache.Length);
if (reader.Read())
{
for (int i = 0; i < cache.Length; ++i)
{
if ((schema[i].AllowDBNull ?? true) && reader.IsDBNull(i))
cache[i] = DBNull.Value;
else cache[i] = materializers[i]();
}
return true;
}
return false;
}
public override void Close() => reader.Close();
public async override Task CloseAsync() => await reader.CloseAsync().ConfigureAwait(false);
public override DataTable GetSchemaTable() => reader.GetSchemaTable();
public async override Task<DataTable> GetSchemaTableAsync(CancellationToken cancellationToken = default) => await reader.GetSchemaTableAsync(cancellationToken).ConfigureAwait(false);
public async override Task<ReadOnlyCollection<DbColumn>> GetColumnSchemaAsync(CancellationToken cancellationToken = default) => await reader.GetColumnSchemaAsync(cancellationToken).ConfigureAwait(false);
public async override Task<bool> NextResultAsync(CancellationToken cancellationToken) => await reader.NextResultAsync(cancellationToken).ConfigureAwait(false);
public async override Task<bool> ReadAsync(CancellationToken cancellationToken)
{
Array.Clear(cache, 0, cache.Length);
if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
for (int i = 0; i < cache.Length; ++i)
{
if ((schema[i].AllowDBNull ?? true) && await reader.IsDBNullAsync(i, cancellationToken).ConfigureAwait(false))
cache[i] = DBNull.Value;
else cache[i] = materializers[i]();
}
return true;
}
return false;
}
}
}
我现在无法提供基准,希望有人能在评论中提供。
添加到@rducom 给出的答案中。 Microsoft.EntityFrameworkCore 6.0.0
阻塞部分实际上是 SqlClient
,@AndriySvyryd 推荐的适用于 EF 核心项目的解决方法是:
Don't use VARCHAR(MAX) or don't use async queries.
我在使用 async
查询读取大型 JSON 对象和图像(二进制)数据时发生了这种情况。
链接:
https://github.com/dotnet/efcore/issues/18571#issuecomment-545992812
https://github.com/dotnet/efcore/issues/18571
https://github.com/dotnet/efcore/issues/885
我的快速修复方法是将调用包装在一个任务中,而只使用同步方法。
这不是一个通用的解决方案,但对于小的聚合,它可以限制在应用程序的一小部分。