【译】ASP.NET|【译】ASP.NET Core 6 中的性能改进
原文 | Brennan Conroy
翻译 | 郑子铭
受到 Stephen Toub 关于 .NET 性能的博文的启发,我们正在写一篇类似的文章来强调 6.0 中对 ASP.NET Core 所做的性能改进。
基准设置
我们将在整个示例中使用 BenchmarkDotNet。在 https://github.com/BrennanConroy/BlogPost60Bench 上提供了一个 repo,其中包括本文中使用的大部分基准。
这篇文章中的大多数基准测试结果都是使用以下命令行生成的:
dotnet run -c Release -f net48 --runtimes net48 netcoreapp3.1 net5.0 net6.0
然后从列表中选择要运行的特定基准。
这告诉 BenchmarkDotNet:
- 在发布配置中构建所有内容。
- 针对 .NET Framework 4.8 外围区域构建它。
- 在 .NET Framework 4.8、.NET Core 3.1、.NET 5 和 .NET 6 上运行每个基准测试。
dotnet run -c Release -f net6.0 --runtimes net6.0
而对于其他版本,只运行了其中的一个子集,例如
dotnet run -c Release -f net5.0 --runtimes net5.0 net6.0
我将包括用于运行每个基准测试的命令当他们出现时。
帖子中的大部分结果都是通过在 Windows 上运行上述基准测试生成的,主要是为了将 .NET Framework 4.8 包含在结果集中。但是,除非另有说明,否则所有这些基准测试通常在 Linux 或 macOS 上运行时都显示出相当的改进。只需确保您已安装要测量的每个运行时。基准测试是在夜间构建的 .NET 6 RC1 以及最新发布的 .NET 5 和 .NET Core 3.1 下载中运行的。
Span
自从在 .NET 2.1 中添加 Span 以来的每个版本,我们都转换了更多代码以在内部和作为公共 API 的一部分使用跨度以提高性能。本次发布也不例外。
PR dotnet/aspnetcore#28855 在添加两个 PathString 实例时删除了来自 string.SubString 的 PathString 中的临时字符串分配,而是使用 Span 作为临时字符串。在下面的基准测试中,我们使用一个短字符串和一个较长的字符串来显示避免使用临时字符串的性能差异。
dotnet run -c Release -f net48 --runtimes net48 net5.0 net6.0 --filter *PathStringBenchmark*
private PathString _first = new PathString("/first/");
private PathString _second = new PathString("/second/");
private PathString _long = new PathString("/longerpathstringtoshowsubstring/");
[Benchmark]
public PathString AddShortString()
{
return _first.Add(_second);
}[Benchmark]
public PathString AddLongString()
{
return _first.Add(_long);
}
Method | Runtime | Toolchain | Mean | Ratio | Allocated |
---|---|---|---|---|---|
AddShortString | .NET Framework 4.8 | net48 | 23.51 ns | 1.00 | 96 B |
AddShortString | .NET 5.0 | net5.0 | 22.73 ns | 0.97 | 96 B |
AddShortString | .NET 6.0 | net6.0 | 14.92 ns | 0.64 | 56 B |
AddLongString | .NET Framework 4.8 | net48 | 30.89 ns | 1.00 | 201 B |
AddLongString | .NET 5.0 | net5.0 | 25.18 ns | 0.82 | 192 B |
AddLongString | .NET 6.0 | net6.0 | 15.69 ns | 0.51 | 104 B |
dotnet run -c Release -f net6.0 --runtimes net6.0 --filter *QueryEnumerableBenchmark*
#if NET6_0_OR_GREATER
public enum QueryEnum
{
Simple = 1,
Encoded,
}[ParamsAllValues]
public QueryEnum QueryParam { get;
set;
}private string SimpleQueryString = "?key1=value1&key2=value2";
private string QueryStringWithEncoding = "?key1=valu%20&key2=value%20";
[Benchmark(Baseline= true)]
public void QueryHelper()
{
var queryString = QueryParam == QueryEnum.Simple ? SimpleQueryString : QueryStringWithEncoding;
foreach (var queryParam in QueryHelpers.ParseQuery(queryString))
{
_ = queryParam.Key;
_ = queryParam.Value;
}
}[Benchmark]
public void QueryEnumerable()
{
var queryString = QueryParam == QueryEnum.Simple ? SimpleQueryString : QueryStringWithEncoding;
foreach (var queryParam in new QueryStringEnumerable(queryString))
{
_ = queryParam.DecodeName();
_ = queryParam.DecodeValue();
}
}
#endif
Method | QueryParam | Mean | Ratio | Allocated |
---|---|---|---|---|
QueryHelper | Simple | 243.13 ns | 1.00 | 360 B |
QueryEnumerable | Simple | 91.43 ns | 0.38 | – |
QueryHelper | Encoded | 351.25 ns | 1.00 | 432 B |
QueryEnumerable | Encoded | 197.59 ns | 0.56 | 152 B |
@paulomorgado 的 dotnet/aspnetcore#29448 使用 string.Create 方法,如果您知道字符串的最终大小,则该方法允许在创建字符串后对其进行初始化。这用于删除 UriHelper.BuildAbsolute 中的一些临时字符串分配。
dotnet run -c Release -f netcoreapp3.1 --runtimes netcoreapp3.1 net6.0 --filter *UriHelperBenchmark*
#if NETCOREAPP
[Benchmark]
public void BuildAbsolute()
{
_ = UriHelper.BuildAbsolute("https", new HostString("localhost"));
}
#endif
Method | Runtime | Toolchain | Mean | Ratio | Allocated |
---|---|---|---|---|---|
BuildAbsolute | .NET Core 3.1 | netcoreapp3.1 | 92.87 ns | 1.00 | 176 B |
BuildAbsolute | .NET 6.0 | net6.0 | 52.88 ns | 0.57 | 64 B |
dotnet run -c Release -f net48 --runtimes net48 netcoreapp3.1 net5.0 net6.0 --filter *ContentDispositionBenchmark*
[Benchmark]
public void ParseContentDispositionHeader()
{
var contentDisposition = new ContentDispositionHeaderValue("inline");
contentDisposition.FileName = "File?Name.bat";
}
Method | Runtime | Toolchain | Mean | Ratio | Allocated |
---|---|---|---|---|---|
ContentDispositionHeader | .NET Framework 4.8 | net48 | 654.9 ns | 1.00 | 570 B |
ContentDispositionHeader | .NET Core 3.1 | netcoreapp3.1 | 581.5 ns | 0.89 | 536 B |
ContentDispositionHeader | .NET 5.0 | net5.0 | 519.2 ns | 0.79 | 536 B |
ContentDispositionHeader | .NET 6.0 | net6.0 | 295.4 ns | 0.45 | 312 B |
ASP.NET Core 的主要组件之一是托管服务器,它带来了许多需要优化的不同问题。我们将专注于改进 6.0 中的空闲连接,我们在其中进行了许多更改以减少连接等待数据时使用的内存量。
我们进行了三种不同类型的更改,一种是减少连接使用的对象的大小,包括 System.IO.Pipelines、SocketConnections 和 SocketSenders。第二种类型的更改是汇集常用访问的对象,以便我们可以重用旧实例并节省分配。第三种变化是利用所谓的“零字节读取”。这是我们尝试使用零字节缓冲区从连接中读取的地方,如果有可用数据,则读取将返回没有数据,但我们会知道现在有可用数据,并且可以提供一个缓冲区来立即读取该数据。这避免了为可能在将来完成的读取预先分配缓冲区,因此我们可以避免大量分配,直到我们知道数据可用。
dotnet/runtime#49270 将 System.IO.Pipelines 的大小从 ~560 字节减少到 ~368 字节,这减少了 34%,每个连接至少有 2 个管道,所以这是一个巨大的胜利。
dotnet/aspnetcore#31308 重构了 Kestrel 的 Socket 层,以避免一些异步状态机并减少剩余状态机的大小,从而为每个连接节省约 33% 的分配。
dotnet/aspnetcore#30769 删除了每个连接的 PipeOptions 分配并将分配移至连接工厂,因此我们仅在服务器的整个生命周期内分配一个,并为每个连接重用相同的选项。来自@benaadams 的 dotnet/aspnetcore#31311 将 WebSocket 请求中众所周知的标头值替换为内部字符串,这允许在标头解析期间分配的字符串被垃圾收集,从而减少长期 WebSocket 连接的内存使用量。 dotnet/aspnetcore#30771 重构了 Kestrel 中的 Sockets 层,首先避免分配 SocketReceiver 对象 + SocketAwaitableEventArgs 并将其组合成一个对象,这节省了几个字节并导致每个连接分配的唯一对象更少。该 PR 还汇集了 SocketSender 类,因此您现在平均拥有多个核心 SocketSender,而不是为每个连接创建一个。所以在下面的基准测试中,当我们有 10,000 个连接时,我的机器上只分配了 16 个,而不是 10,000 个,这节省了约 46 MB!
另一个类似大小的更改是 dotnet/runtime#49123,它增加了对 SslStream 中零字节读取的支持,因此我们的 10,000 个空闲连接从 SslStream 分配中从 ~46 MB 变为 ~2.3 MB。 dotnet/runtime#49117 在 StreamPipeReader 上添加了对零字节读取的支持,然后 Kestrel 在 dotnet/aspnetcore#30863 中使用它开始在 SslStream 中使用零字节读取。
所有这些变化的结果是大量减少了空闲连接的内存使用量。
以下数字并非来自 BenchmarkDotNet 应用程序,因为它正在测量空闲连接,并且使用客户端和服务器应用程序进行设置更容易。
控制台和 WebApplication 代码粘贴在以下要点中:https://gist.github.com/BrennanConroy/02e8459d63305b4acaa0a021686f54c7
下面是不同框架上服务器上 10,000 个空闲安全 WebSocket 连接 (WSS) 占用的内存量。
Framework | Memory |
---|---|
net48 | 665.4 MB |
net5.0 | 603.1 MB |
net6.0 | 160.8 MB |
Entity Framework Core
EF Core 在 6.0 中进行了一些重大改进,执行查询的速度提高了 31%,而 TechEmpower Fortunes 基准测试通过运行时更新、优化基准测试和 EF 改进提高了 70%。
这些改进来自改进对象池、智能地检查遥测是否启用,以及当您知道您的应用程序安全地使用 DbContext 时添加一个选项以选择退出线程安全检查。
请参阅宣布 Entity Framework Core 6.0 Preview 4:Performance Edition 博客文章,其中详细介绍了许多改进。
Blazor
本地 byte[] 互通 Blazor 现在在执行 JavaScript 互操作时有效地支持字节数组。以前,向 JavaScript 发送和从 JavaScript 发送的字节数组是 Base64 编码的,因此它们可以序列化为 JSON,这增加了传输大小和 CPU 负载。 Base64 编码现已在 .NET 6 中进行了优化,允许用户透明地使用 .NET 中的 byte[] 和 JavaScript 中的 Uint8Array。有关将此功能用于 JavaScript 到 .NET 和 .NET 到 JavaScript 的文档。
让我们看一个快速基准测试,以了解 .NET 5 和 .NET 6 中的 byte[] 互操作之间的区别。以下 Razor 代码创建一个 22 kB byte[],并将其发送到 JavaScript 的 receiveAndReturnBytes 函数,该函数立即返回字节[]。此数据往返重复 10,000 次,并将时间数据打印到屏幕上。此代码与 .NET 5 和 .NET 6 相同。
@Message@code {
public string Message { get;
set;
} = "Press button to benchmark";
private async Task RoundtripData()
{
var bytes = new byte[1024*22];
List timeForInterop = new List();
var testTime = DateTime.Now;
for (var i = 0;
i < 10_000;
i++)
{
var interopTime = DateTime.Now;
var result = await JSRuntime.InvokeAsync("receiveAndReturnBytes", bytes);
timeForInterop.Add(DateTime.Now.Subtract(interopTime).TotalMilliseconds);
}Message = $"Round-tripped: {bytes.Length / 1024d} kB 10,000 times and it took on average {timeForInterop.Average():F3}ms, and in total {DateTime.Now.Subtract(testTime).TotalMilliseconds:F1}ms";
}
}
接下来我们看一下receiveAndReturnBytes JavaScript 函数。在 .NET 5 中。我们必须首先将 Base64 编码的字节数组解码为 Uint8Array,以便它可以在应用程序代码中使用。然后我们必须在将数据返回到服务器之前将其重新编码为 Base64。
function receiveAndReturnBytes(bytesReceivedBase64Encoded) {
const bytesReceived = base64ToArrayBuffer(bytesReceivedBase64Encoded);
// Use Uint8Array data in applicationconst bytesToSendBase64Encoded = base64EncodeByteArray(bytesReceived);
if (bytesReceivedBase64Encoded != bytesToSendBase64Encoded) {
throw new Error("Expected input/output to match.")
}return bytesToSendBase64Encoded;
}// https://stackoverflow.com/a/21797381
function base64ToArrayBuffer(base64) {
const binaryString = atob(base64);
const length = binaryString.length;
const result = new Uint8Array(length);
for (let i = 0;
i < length;
i++) {
result[i] = binaryString.charCodeAt(i);
}
return result;
}function base64EncodeByteArray(data) {
const charBytes = new Array(data.length);
for (var i = 0;
i < data.length;
i++) {
charBytes[i] = String.fromCharCode(data[i]);
}
const dataBase64Encoded = btoa(charBytes.join(''));
return dataBase64Encoded;
}
编码/解码增加了客户端和服务器的大量开销,同时还需要大量的样板代码。那么这将如何在 .NET 6 中完成呢?好吧,它有点简单:
function receiveAndReturnBytes(bytesReceived) {
// bytesReceived comes as a Uint8Array ready for use
// and can be used by the application or immediately returned.
return bytesReceived;
}
所以写起来肯定更容易,但它的表现如何呢?分别在 .NET 5 和 .NET 6 的 blazorserver 模板中运行这些代码片段,在 Release 配置下,我们看到 .NET 6 在 byte[] 互操作方面提供了 78% 的性能提升!
—————– | .NET 6 (ms) | .NET 5 (ms) | Improvement |
---|---|---|---|
Total Time | 5273 | 24463 | 78% |
输入文件 使用上面提到的 Blazor Streaming Interop,我们现在支持通过 InputFile 组件上传大文件(以前上传限制为 ~2GB)。由于原生字节 [] 流而不是通过 Base64 编码,该组件还具有显着的速度改进。例如,与 .NET 5 相比,上传 100 MB 文件的速度提高了 77%。
.NET 6 (ms) | .NET 5 (ms) | Percentage |
---|---|---|
2591 | 10504 | 75% |
2607 | 11764 | 78% |
2632 | 11821 | 78% |
Average: | 77% |
InputFile 组件已升级为通过 dotnet/aspnetcore#33900 使用流式传输。
大杂烩
来自@benaadams 的 dotnet/aspnetcore#30320 对我们的 Typescript 库进行了现代化改造并对其进行了优化,因此网站加载速度更快。 signalr.min.js 文件从 36.8 kB 压缩和 132 kB 未压缩变为 16.1 kB 压缩和 42.2 kB 未压缩。 blazor.server.js 文件压缩后为 86.7 kB,未压缩时为 276 kB,压缩后为 43.9 kB,未压缩时为 130 kB。
@benaadams 的 dotnet/aspnetcore#31322 在从连接功能集合中获取常用功能时删除了一些不必要的强制转换。这在访问集合中的常见特征时提供了约 50% 的改进。不幸的是,实际上不可能在基准测试中看到性能改进,因为它需要一堆内部类型,所以我将在此处包含来自 PR 的数字,如果您有兴趣运行它们,PR 包括可以运行的基准反对内部代码。
Method | Mean | Op/s | Diff |
---|---|---|---|
Get * | 8.507 ns | 117,554,189.6 | +50.0% |
Get * | 9.034 ns | 110,689,963.7 | – |
Get * | 9.466 ns | 105,636,431.7 | +58.7% |
Get * | 10.007 ns | 99,927,927.4 | +50.0% |
Get * | 10.564 ns | 94,656,794.2 | +44.7% |
Method | Branch | Type | Mean | Op/s | Delta |
---|---|---|---|---|---|
GetHeaders | before | Plaintext | 25.793 ns | 38,770,569.6 | – |
GetHeaders | after | Plaintext | 12.775 ns | 78,279,480.0 | +101.9% |
GetHeaders | before | Common | 121.355 ns | 8,240,299.3 | – |
GetHeaders | after | Common | 37.598 ns | 26,597,474.6 | +222.8% |
GetHeaders | before | Unknown | 366.456 ns | 2,728,840.7 | – |
GetHeaders | after | Unknown | 223.472 ns | 4,474,824.0 | +64.0% |
SetHeaders | before | Plaintext | 49.324 ns | 20,273,931.8 | – |
SetHeaders | after | Plaintext | 34.996 ns | 28,574,778.8 | +40.9% |
SetHeaders | before | Common | 635.060 ns | 1,574,654.3 | – |
SetHeaders | after | Common | 108.041 ns | 9,255,723.7 | +487.7% |
SetHeaders | before | Unknown | 1,439.945 ns | 694,470.8 | – |
SetHeaders | after | Unknown | 517.067 ns | 1,933,985.7 | +178.4% |
Branch Type | Allocations | Bytes |
---|---|---|
Before | CancellationTokenSource | 98,314 4,719,072 |
After | CancellationTokenSource | 125 6,000 |
dotnet/aspnetcore#316600 通过为整个流重用分配的 StreamItem 对象而不是为每个流项分配一个对象,改进了 SignalR 中服务器到客户端流的性能。并且 dotnet/aspnetcore#31661 将 HubCallerClients 对象存储在 SignalR 连接上,而不是为每个 Hub 方法调用分配它。
@ShreyasJejurkar 的 dotnet/aspnetcore#31506 重构了 WebSocket 握手的内部结构,以避免临时 List 分配。 @gfoidl 中的 dotnet/aspnetcore#32829 重构了 QueryCollection 以减少分配并矢量化一些代码。 @benaadams 的 dotnet/aspnetcore#32234 删除了 HttpRequestHeaders 枚举中未使用的字段,该字段通过不再为每个枚举的标头分配字段来提高性能。
来自 martincostello 的 dotnet/aspnetcore#31333 将 Http.Sys 转换为使用 LoggerMessage.Define,这是高性能日志记录 API。这避免了不必要的值类型装箱、日志格式字符串的解析,并且在某些情况下避免了在日志级别未启用时分配字符串或对象。
dotnet/aspnetcore#31784 添加了一个新的 IApplicationBuilder。使用重载来注册中间件,以避免在运行中间件时进行一些不必要的按请求分配。旧代码如下所示:
app.Use(async (context, next) =>
{
await next();
});
新代码如下所示:
app.Use(async (context, next) =>
{
await next(context);
});
下面的基准测试模拟了中间件管道,而没有设置服务器来展示改进。使用 int 代替 HttpContext 进行请求,中间件返回完成的任务。
dotnet run -c Release -f net6.0 --runtimes net6.0 --filter *UseMiddlewareBenchmark*
static private Func, Func> UseOld(Func, Task> middleware)
{
return next =>
{
return context =>
{
Func simpleNext = () => next(context);
return middleware(context, simpleNext);
};
};
}static private Func, Func> UseNew(Func, Task> middleware)
{
return next => context => middleware(context, next);
}Func Middleware = UseOld((c, n) => n())(i => Task.CompletedTask);
Func NewMiddleware = UseNew((c, n) => n(c))(i => Task.CompletedTask);
[Benchmark(Baseline = true)]
public Task Use()
{
return Middleware(10);
}[Benchmark]
public Task UseNew()
{
return NewMiddleware(10);
}
Method | Mean | Ratio | Allocated |
---|---|---|---|
Use | 15.832 ns | 1.00 | 96 B |
UseNew | 2.592 ns | 0.16 | – |
原文链接 Performance improvements in ASP.NET Core 6
文章图片
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
【【译】ASP.NET|【译】ASP.NET Core 6 中的性能改进】如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。
推荐阅读
- 玩转JAVA系列|【JavaSE】集合框架及背后的数据结构
- Java开发|05-JavaSE【泛型,数据结构,List接口,Set接口,Collections工具类】
- 【JS30-Wes Bos】HTML5 画板 06
- 【C语言】指针详解
- CAN201网络
- 第一阶段|【第一阶段 day22 面向对象】面向过程 面向对象 类 对象 类与对象的关系 对象创建过程分析 封装 访问控制符
- COMP1038 银行系统
- 【Copy攻城狮日志】React|【Copy攻城狮日志】React Native 集成 HMS Core
- 酒店用的电视桌面系统有什么样的()
- 腾讯云荣获第六届CSA两项大奖,护航数字化转型获行业认可