C# async/await 读取 DbDataReader 的效率(或滥用)
C# async/await efficiency (or abusing) on reading DbDataReader
偶然发现了一段相对常用的代码,起初看起来效率很低。 (我知道有时优化可能是邪恶的,但我想知道)
简介部分 - 相当简单的 SP 执行 + 读取返回的数据:
try
{
await connection.OpenAsync();
using (var command = connection.CreateCommand())
{
command.CommandText = sql.ToString();
command.Parameters.AddRange(sqlParameters.ToArray());
var reader = await command.ExecuteReaderAsync();
if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var item = await GetProjectElement(reader);
list.Add(item);
}
}
reader.Dispose();
}
}
finally
{
connection.Close();
}
担心的是函数
await GetProjectElement(reader)
private async Task<Project> GetProjectElement(DbDataReader reader)
{
var item = new Project
{
Id = await reader.GetFieldValueAsync<int>(1),
ParentId = await reader.IsDBNullAsync(2) ? default(int?) : await reader.GetFieldValueAsync<int>(2),
Name = await reader.IsDBNullAsync(3) ? default(string) : await reader.GetFieldValueAsync<string>(3),
Description = await reader.IsDBNullAsync(4) ? default(string) : await reader.GetFieldValueAsync<string>(4),
Address = await reader.IsDBNullAsync(5) ? default(string) : await reader.GetFieldValueAsync<string>(5),
City = await reader.IsDBNullAsync(6) ? default(string) : await reader.GetFieldValueAsync<string>(6),
PostalCode = await reader.IsDBNullAsync(7) ? default(string) : await reader.GetFieldValueAsync<string>(7),
Type = (ProjectTypeEnum)(await reader.GetFieldValueAsync<byte>(8)),
StartDate = await reader.IsDBNullAsync(9) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(9),
EstimatedEndDate = await reader.IsDBNullAsync(10) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(10),
ActualEndDate = await reader.IsDBNullAsync(11) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(11),
WebsiteUrl = await reader.IsDBNullAsync(12) ? default(string) : await reader.GetFieldValueAsync<string>(12),
Email = await reader.IsDBNullAsync(13) ? default(string) : await reader.GetFieldValueAsync<string>(13),
PhoneNumber = await reader.IsDBNullAsync(14) ? default(string) : await reader.GetFieldValueAsync<string>(14),
MobilePhoneNumber = await reader.IsDBNullAsync(15) ? default(string) : await reader.GetFieldValueAsync<string>(15),
Key = await reader.IsDBNullAsync(16) ? default(Guid?) : await reader.GetFieldValueAsync<Guid>(16),
OrganizationElementId = await reader.GetFieldValueAsync<int>(17),
CompanyOrganizationElementId = await reader.IsDBNullAsync(18) ? default(int?) : await reader.GetFieldValueAsync<int>(18),
IsArchived = await reader.GetFieldValueAsync<bool>(20),
IsDeleted = await reader.GetFieldValueAsync<bool>(21),
CreatedOn = await reader.GetFieldValueAsync<DateTime>(22),
CreatedBy = await reader.GetFieldValueAsync<string>(23),
ModifiedOn = await reader.IsDBNullAsync(24) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(24),
ModifiedBy = await reader.IsDBNullAsync(25) ? default(string) : await reader.GetFieldValueAsync<string>(25)
};
return item;
}
如您所见,有很多等待调用被编译器转换为状态机,不是吗?
您可以找到编译器生成代码的简化版本 here。
大量的 GOTO,这意味着上下文一遍又一遍地切换。
由于 SP 是在未指定 CommandBehavior 的情况下执行的 - 数据将处于非顺序模式。 (可能的原因是 table 行在 Project
link 的这种情况下不应该是非常大的字节)
我的问题是:
1) 这种滥用 async/await 是不是没有明显的原因,因为行数据已经缓冲在内存中,对吗?
2) 在这种情况下 Task<Project>
是纯开销吗?
3) 与没有 await
ing
的方法相比,这种方法实际上有更差的性能吗
最后的想法:如果我做对了,我们希望对大 table 行使用 CommandBehavior.SequentialAccess,其中内容可能超过合理的长度,因此让我们想要异步阅读它? (比如存储 varbinary(max) 或 blob)
正如其他人所指出的,GOTO 不会导致上下文切换,而且速度非常快。
1) is this abusing of the async/await without an obvious reason, because the row data is already buffered in memory, right?
ADO.NET 允许实现者在如何精确地实现基类型方面有很大的回旋余地。也许该行在内存中,也许不在。
2) is Task a pure overhead in this scenario?
是的,如果操作实际上是同步的。这是您的 ADO.NET 提供商的实施细节。
请注意,状态机和 await
在这里几乎没有增加开销;有一个 async fast path 如果可能,代码只是保持同步执行。
3) would this approach actually have a worse performance compared to one without awaiting
可能不会。首先,性能影响不会受到调用每个方法并继续同步执行的少量 CPU 工作的影响。您看到的任何性能影响都是由于在 Gen0 堆上抛出额外的 Task<T>
个实例并且必须进行垃圾回收。这就是现在有 ValueTask<T>
.
的原因
但即使是这种性能影响也很可能在对数据库服务器的网络 I/O 调用旁边不明显。也就是说,如果您想了解微观性能损失,Zen of Async 是经典之作。
偶然发现了一段相对常用的代码,起初看起来效率很低。 (我知道有时优化可能是邪恶的,但我想知道)
简介部分 - 相当简单的 SP 执行 + 读取返回的数据:
try
{
await connection.OpenAsync();
using (var command = connection.CreateCommand())
{
command.CommandText = sql.ToString();
command.Parameters.AddRange(sqlParameters.ToArray());
var reader = await command.ExecuteReaderAsync();
if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var item = await GetProjectElement(reader);
list.Add(item);
}
}
reader.Dispose();
}
}
finally
{
connection.Close();
}
担心的是函数
await GetProjectElement(reader)
private async Task<Project> GetProjectElement(DbDataReader reader)
{
var item = new Project
{
Id = await reader.GetFieldValueAsync<int>(1),
ParentId = await reader.IsDBNullAsync(2) ? default(int?) : await reader.GetFieldValueAsync<int>(2),
Name = await reader.IsDBNullAsync(3) ? default(string) : await reader.GetFieldValueAsync<string>(3),
Description = await reader.IsDBNullAsync(4) ? default(string) : await reader.GetFieldValueAsync<string>(4),
Address = await reader.IsDBNullAsync(5) ? default(string) : await reader.GetFieldValueAsync<string>(5),
City = await reader.IsDBNullAsync(6) ? default(string) : await reader.GetFieldValueAsync<string>(6),
PostalCode = await reader.IsDBNullAsync(7) ? default(string) : await reader.GetFieldValueAsync<string>(7),
Type = (ProjectTypeEnum)(await reader.GetFieldValueAsync<byte>(8)),
StartDate = await reader.IsDBNullAsync(9) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(9),
EstimatedEndDate = await reader.IsDBNullAsync(10) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(10),
ActualEndDate = await reader.IsDBNullAsync(11) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(11),
WebsiteUrl = await reader.IsDBNullAsync(12) ? default(string) : await reader.GetFieldValueAsync<string>(12),
Email = await reader.IsDBNullAsync(13) ? default(string) : await reader.GetFieldValueAsync<string>(13),
PhoneNumber = await reader.IsDBNullAsync(14) ? default(string) : await reader.GetFieldValueAsync<string>(14),
MobilePhoneNumber = await reader.IsDBNullAsync(15) ? default(string) : await reader.GetFieldValueAsync<string>(15),
Key = await reader.IsDBNullAsync(16) ? default(Guid?) : await reader.GetFieldValueAsync<Guid>(16),
OrganizationElementId = await reader.GetFieldValueAsync<int>(17),
CompanyOrganizationElementId = await reader.IsDBNullAsync(18) ? default(int?) : await reader.GetFieldValueAsync<int>(18),
IsArchived = await reader.GetFieldValueAsync<bool>(20),
IsDeleted = await reader.GetFieldValueAsync<bool>(21),
CreatedOn = await reader.GetFieldValueAsync<DateTime>(22),
CreatedBy = await reader.GetFieldValueAsync<string>(23),
ModifiedOn = await reader.IsDBNullAsync(24) ? default(DateTime?) : await reader.GetFieldValueAsync<DateTime>(24),
ModifiedBy = await reader.IsDBNullAsync(25) ? default(string) : await reader.GetFieldValueAsync<string>(25)
};
return item;
}
如您所见,有很多等待调用被编译器转换为状态机,不是吗?
您可以找到编译器生成代码的简化版本 here。 大量的 GOTO,这意味着上下文一遍又一遍地切换。
由于 SP 是在未指定 CommandBehavior 的情况下执行的 - 数据将处于非顺序模式。 (可能的原因是 table 行在 Project
link 的这种情况下不应该是非常大的字节)
我的问题是:
1) 这种滥用 async/await 是不是没有明显的原因,因为行数据已经缓冲在内存中,对吗?
2) 在这种情况下 Task<Project>
是纯开销吗?
3) 与没有 await
ing
最后的想法:如果我做对了,我们希望对大 table 行使用 CommandBehavior.SequentialAccess,其中内容可能超过合理的长度,因此让我们想要异步阅读它? (比如存储 varbinary(max) 或 blob)
正如其他人所指出的,GOTO 不会导致上下文切换,而且速度非常快。
1) is this abusing of the async/await without an obvious reason, because the row data is already buffered in memory, right?
ADO.NET 允许实现者在如何精确地实现基类型方面有很大的回旋余地。也许该行在内存中,也许不在。
2) is Task a pure overhead in this scenario?
是的,如果操作实际上是同步的。这是您的 ADO.NET 提供商的实施细节。
请注意,状态机和 await
在这里几乎没有增加开销;有一个 async fast path 如果可能,代码只是保持同步执行。
3) would this approach actually have a worse performance compared to one without awaiting
可能不会。首先,性能影响不会受到调用每个方法并继续同步执行的少量 CPU 工作的影响。您看到的任何性能影响都是由于在 Gen0 堆上抛出额外的 Task<T>
个实例并且必须进行垃圾回收。这就是现在有 ValueTask<T>
.
但即使是这种性能影响也很可能在对数据库服务器的网络 I/O 调用旁边不明显。也就是说,如果您想了解微观性能损失,Zen of Async 是经典之作。