原文 | 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 上运行每个基准测试。
对于某些基准测试,它们仅在 .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 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 AllocatedAddShortString .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 AllocatedQueryHelper 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