ASP.NET Core 6有哪些具体性能优化措施?

2026-05-23 00:031阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计2847个文字,预计阅读时间需要12分钟。

原文+ | Brennan Conroy 翻译+ | 邱子铿 受到 Stephen Toub 关于 .NET 性能的博客文章的启发,我们正在撰写一篇类似的文章,来强化 6.0 中对 ASP.NET Core 所做的性能改进。基本设置+ | 我们将在整个示例中使用。

原文 | Brennan Conroy

翻译 | 郑子铭

受到 Stephen Toub 关于 .NET 性能的博文的启发,我们正在写一篇类似的文章来强调 6.0 中对 ASP.NET Core 所做的性能改进。

基准设置

我们将在整个示例中使用 BenchmarkDotNet。在 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

自从在 .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/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 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

重要的是要注意没有免费的午餐。在新的 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("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

从 net5.0 到 net6.0,内存减少了近 4 倍!

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 相同。

<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; } // 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%

此外,框架内利用了这种字节数组互操作支持,以实现 JavaScript 和 .NET 之间的双向流式互操作。用户现在可以传输任意二进制数据。有关从 .NET 流式传输到 JavaScript 的文档可在此处获得,JavaScript 到 .NET 文档可在此处获得。

输入文件

使用上面提到的 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

如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。

本文共计2847个文字,预计阅读时间需要12分钟。

原文+ | Brennan Conroy 翻译+ | 邱子铿 受到 Stephen Toub 关于 .NET 性能的博客文章的启发,我们正在撰写一篇类似的文章,来强化 6.0 中对 ASP.NET Core 所做的性能改进。基本设置+ | 我们将在整个示例中使用。

原文 | Brennan Conroy

翻译 | 郑子铭

受到 Stephen Toub 关于 .NET 性能的博文的启发,我们正在写一篇类似的文章来强调 6.0 中对 ASP.NET Core 所做的性能改进。

基准设置

我们将在整个示例中使用 BenchmarkDotNet。在 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

自从在 .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/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 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

重要的是要注意没有免费的午餐。在新的 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("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

从 net5.0 到 net6.0,内存减少了近 4 倍!

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 相同。

<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; } // 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%

此外,框架内利用了这种字节数组互操作支持,以实现 JavaScript 和 .NET 之间的双向流式互操作。用户现在可以传输任意二进制数据。有关从 .NET 流式传输到 JavaScript 的文档可在此处获得,JavaScript 到 .NET 文档可在此处获得。

输入文件

使用上面提到的 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

如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。