关于asp.net-core:ASPNET-Core-6-的性能改进

39次阅读

共计 11676 个字符,预计需要花费 30 分钟才能阅读完成。

受到 由 Stephen Toub 公布的对于 .NET 性能的博客的启发,咱们正在写一篇相似的文章来强调 ASP.NET Core 在 6.0 中所做的性能改良。

基准设置

咱们整个过程中大部分的实例应用的是 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 上运行每个基准测试。
  • 对于某些基准测试,它们仅在 .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< T >

自从在.NET 2.1 中减少了 Span< T >,之后的每一个版本咱们都转换了更多的代码以在外部和作为公共 API 的一部分应用 Span 来进步性能。这次公布也不例外。

PR dotnet/aspnetcore#28855 在增加两个 PathString 实例时删除了来自 string.SubString 的 PathString 中的长期字符串调配,而是应用 Span\&lt;char\&gt; 作为长期字符串。在上面的基准测试中,咱们应用一个短字符串和一个长字符串来显示防止应用长期字符串的性能差别。

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);
}
办法 运行 工具链 平均分配 比率 已调配
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/aspnetcore#34001 引入了一个新的基于 Span 的 API,用于枚举查问字符串,在没有编码字符的常见状况下,该查问字符串是调配闲暇的,当查问字符串蕴含编码字符时,调配更低。

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
办法 查问参数 平均分配 比率 已调配
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

须要留神的是,天下没有收费的午餐。在新的 QueryStringEnumerable API 的状况下,如果您打算屡次枚举查问字符串值,它实际上可能比应用 QueryHelpers.ParseQuery 并存储已解析查问字符串值的字典更低廉。

@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
办法 运行 工具链 平均分配 比率 已调配
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

PR dotnet/aspnetcore#31267 将 ContentDispositionHeaderValue 中的一些解析逻辑转换为应用基于 Span\&lt;T\&gt; 的 API,以防止在常见状况下呈现长期字符串和长期 byte[]。

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";
 }
办法 运行 工具链 均匀 比例 已调配
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 个,这节俭了~ 46mb !

另一个相似的大小变动是 dotnet/runtime#49123,它减少了对 SslStream 中零字节读取的反对,这样咱们的 10,000 个闲暇连贯从 SslStream 调配的~ 46mb 到~2.3 MB。dotnet/runtime#49117 在 StreamPipeReader 上增加了对零字节读取的反对,而后 Kestrel 在 dotnet/aspnetcore#30863 中应用它开始在 SslStream 中应用零字节读取。

所有这些变动的最终后果是大量缩小闲暇连贯的内存应用。

上面的数字不是来自于 BenchmarkDotNet 应用程序,因为它测量闲暇连贯,而且更容易用客户机和服务器应用程序进行设置。

控制台和 WebApplication 代码粘贴在以下要点中:

https://gist.github.com/BrennanConroy/02e8459d63305b4acaa0a021686f54c7

上面是 10000 个闲暇的平安 WebSocket 连贯 (WSS) 在不同框架上占用服务器的内存。

框架 内存
net48 665.4 MB
net5.0 603.1 MB
net6.0 160.8 MB

这比 net5 缩小了近 4 倍的内存。

实体框架外围

EF Core 在 6.0 版本中做了大量的改良,查问执行速度进步了 31%,TechEmpower fortune 的基准运行工夫更新、优化基准和 EF 的改良进步了 70%。

这些改良来自于对象池的改良,智能查看是否启用了遥测技术,以及增加一个选项,当你晓得你的应用程序平安地应用 DbContext 时,能够抉择退出线程安全检查。

请参阅公布实体框架外围 6.0 预览版 4: 性能版的博客文章,其中具体强调了许多改良。

Blazor

本机 byte[] 互操作

Blazor 当初在执行 JavaScript 互操作时对字节数组有了无效的反对。以前,发送到和从 JavaScript 的字节数组是 Base64 编码的,因而它们能够被序列化为 JSON,这减少了传输大小和 CPU 负载。Base64 编码当初曾经在.NET6 中进行了优化,容许用户通明地应用.NET 中的 byte[]和 JavaScript 中的 Uint8Array。阐明如何将此个性用于 JavaScript 到.NET 和.NET 到 JavaScript。

让咱们看一个疾速的基准测试,看看 byte[]互操作在.NET 5 和.NET 6 中的区别。以下 Razor 代码创立了一个 22 kB 的字节[],并将其发送给 JavaScript 的 receiveAndReturnBytes 函数,该函数立刻返回字节[]。这种数据往返反复了 10,000 次,工夫数据被打印到屏幕上。这段代码对于.NET 5 和.NET 6 是雷同的。

<button @onclick="@RoundtripData">Roundtrip Data</button>
<hr />
@Message
@code {public string Message { get; set;} = "Press button to benchmark";
    private async Task RoundtripData()
    {var bytes = new byte[1024*22];
        List<double> timeForInterop = new List<double>();
        var testTime = DateTime.Now;
        for (var i = 0; i < 10_000; i++)
        {
            var interopTime = DateTime.Now;
            var result = await JSRuntime.InvokeAsync<byte[]>("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 application
    const 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% 的性能晋升!

请留神,流式互操作反对还能够无效下载(大)文件,无关更多详细信息,请参阅文档。

InputFile 组件已降级为通过 dotnet/aspnetcore#33900 应用流式传输。

—————– .NET 6 (ms) .NET 5 (ms) 晋升
总工夫 5273 24463 78%

此外,这个字节数组互操作反对在框架中被用来反对 JavaScript 和.NET 之间的双向流互操作。用户当初可能传输任意二进制数据。无关从 .NET 流式传输到 JavaScript 的文档可在此处取得,JavaScript 到 .NET 文档可在此处取得。

输出文件

应用下面提到的 Blazor Streaming Interop,咱们当初反对通过 InputFile 组件上传大文件 (以前的上传限度在 2GB 左右)。因为应用了本地 byte[] 流,而不是应用 Base64 编码,该组件的速度也有了显著进步。例如,例如,与.NET 5 相比,一个 100mb 文件的上传速度要快 77%。

.NET 6 (ms) .NET 5 (ms) 百分比
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 包含能够运行的基准拥护外部代码。

dotnet/aspnetcore#31519 也来自 @benaadams,将默认接口办法增加到 IHeaderDictionary 类型,以通过以标头名称命名的属性拜访公共标头。拜访题目字典时不再输出谬误的常见题目!这篇博客文章中更乏味的是,这个扭转容许服务器实现返回一个自定义标头字典,以更优化地实现这些新的接口办法。例如,服务器可能会将标头值间接存储在一个字段中,并间接返回该字段,而不是在外部字典中查问标头值,这须要对键进行哈希并查找条目。在某些状况下,当获取或设置标头值时,此更改可带来高达 480% 的改良。再一次,为了正确地对这个变动进行基准测试,以显示它须要应用外部类型进行设置,所以我将包含来自 PR 的数字,对于那些有趣味尝试它的人来说,PR 蕴含在外部代码上运行的基准测试。

办法 分支 类型 均匀 操作 / 秒 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%

dotnet/aspnetcore#31466 应用 .NET 6 中引入的新 CancellationTokenSource.TryReset() 办法在连贯敞开但未勾销的状况下重用 CancellationTokenSource。上面的数字是通过运行 bombardier 对 Kestrel 的 125 个连贯收集的,它运行了大概 10 万个申请。

分支 类型 调配 字节数
Before CancellationTokenSource 98,314 4,719,072
After CancellationTokenSource 125 6,000

dotnet/aspnetcore#31528 和 dotnet/aspnetcore#34075 别离对重用 HTTPS 握手和 HTTP3 流的 CancellationTokenSource 做了相似的更改。

dotnet/aspnetcore#31660 通过在 SignalR 中为整个流重用调配的 StreamItem 对象,而不是为每个流项调配一个,进步了服务器对客户端流的性能。而 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<int, Task>, Func<int, Task>> UseOld(Func<int, Func<Task>, Task> middleware)
{
    return next =>
    {
        return context =>
        {Func<Task> simpleNext = () => next(context);
            return middleware(context, simpleNext);
        };
    };
}
static private Func<Func<int, Task>, Func<int, Task>> UseNew(Func<int, Func<int, Task>, Task> middleware)
{return next => context => middleware(context, next);
}
Func<int, Task> Middleware = UseOld((c, n) => n())(i => Task.CompletedTask);
Func<int, Task> 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);
}
办法 均匀 比率 已调配
Use 15.832 ns 1.00 96 B
UseNew 2.592 ns 0.16

总结

心愿您喜爱浏览 ASP.NET Core 6.0 中的一些改良! 我激励你去看看.NET 6 博客中对于运行时性能改良的文章。

正文完
 0