乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 获取.Net 7并查看.Net 7中的性能提升(简中译文)
什么是.Net 7
.Net 7目前是.Net实现的最新版本,暂时还是预览阶段,已经更新到Preview 7。
获取.Net 7
Windows
MacOS
安装.Net 7
验证安装:
dotnet --info
安装之前
安装之后
.NET 7中的性能改进
Stephen Toub - MSFT
一年前,我发表了《.NET 6的性能改进》,紧接着又发表了针对.NET 5、.NET Core 3.0、.NET Core 2.1和.NET Core 2.0的类似文章。我喜欢写这些帖子,也喜欢看开发者对它们的回应。去年有一条评论特别引起了我的共鸣。该评论者引用了《虎胆龙威》电影中的一句话:"'当亚历山大看到他的领域的广度时,他哭了,因为没有更多的世界可以征服'",并质疑.NET的性能改进是否类似。是否已经干涸了吗?是否没有更多的"[性能]
世界可以征服 "了?我有点眩晕地说,即使.NET 6的速度有多快,.NET 7也明确地强调了还可以和已经做了多少。
与以前的.NET版本一样,性能是贯穿整个堆栈的关键焦点,无论是明确为性能而创建的功能,还是与性能无关的功能,在设计和实现时都敏锐地考虑到了性能。而现在,.NET 7的候选发布版(Release Candidate)就在眼前,这是一个讨论其中许多内容的好时机。在过去的一年中,每当我审查一个可能对性能产生积极影响的PR时,我都会把这个链接复制到我维护的日记中,以便写这篇文章。几周前,当我坐下来写这篇文章时,我面对的是一份几乎有1000个影响性能的PRs的清单(在7000多个PRs中,有一个进入了发布阶段),我很高兴能在这里与你分享其中的近500个。
在我们深入讨论之前,有一个想法。在过去的几年里,我收到过一些关于我的一些以业绩为重点的文章长度的负面反馈,虽然我不同意这些批评,但我尊重这些意见。因此,今年,请考虑这是一次"选择你自己的冒险(choose your own adventure.)"。如果你在这里只是想找一个超短的冒险,一个提供顶层总结和核心信息的冒险,我很乐意满足你的要求。
.NET 7是快速的。真的很快。在这个版本中,有一千个影响性能的PR进入了运行时和核心库,更不用说ASP.NET Core和Windows Forms以及Entity Framework等方面的所有改进。这是有史以来最快的.NET。如果你的经理问你为什么你的项目应该升级到.NET 7,你可以说:"除了版本中的所有新功能,.NET 7还超级快。"
或者,如果你喜欢稍微长一点的冒险,一个充满了有趣的以性能为重点的数据块的冒险,可以考虑略过这篇文章,寻找小的代码片段和相应的表格,显示大量可衡量的性能改进。在这一点上,你也可以带着你的头和我的感谢离开。
这两条路都实现了我花时间写这些帖子的主要目标之一,即强调下一个版本的伟大之处,并鼓励每个人都去尝试一下。但是,我对这些帖子也有其他目标。我希望每个感兴趣的人都能从这篇帖子中了解到.NET是如何实现的,为什么会做出各种决定,评估了各种权衡,采用了哪些技术,考虑了哪些算法,以及利用了哪些有价值的工具和方法来使.NET比以前更快。我希望开发者能从我们自己的学习中得到启发,并找到方法将这些新发现的知识应用到他们自己的代码库中,从而进一步提高生态系统中代码的整体性能。我希望开发者能多花点时间,在下一次处理棘手问题时,考虑伸手去拿分析器,考虑看看他们正在使用的组件的源代码,以便更好地了解如何使用它,并考虑重新审视以前的假设和决定,以确定它们是否仍然准确和适当。我希望开发者对提交PR以改善.NET的前景感到兴奋,这不仅是为了他们自己,也是为了全球每个使用.NET的开发者。如果这些听起来很有趣,那么我鼓励你选择最后一次冒险:准备一壶你最喜欢的热饮,舒服一点,并请享受。
(哦,请不要把这个打印到纸上。"打印成PDF"告诉我这将需要三分之一的卷轴)。
安装
这篇文章中的微观基准利用了benchmarkdotnet
。为了使你能够轻松地进行自己的验证,我对我使用的基准有一个非常简单的设置。创建一个新的C#项目。
dotnet new console -o benchmarks
cd benchmarks
你的新的benchmarks
目录中将包含一个benchmarks.csproj
项目文件和一个Program.cs
文件。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
我们可以借助Visual Studio打开benchmarks.csproj
文件,并替换成如下部分:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net7.0;net6.0</TargetFrameworks>
<LangVersion>Preview</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="benchmarkdotnet" Version="0.13.2" />
</ItemGroup>
</Project>
在上诉内容中,需要留意到TargetFramework
变成了TargetFrameworks
,并且在其中增加了net6.0
作为兼容目标。
并且Program.cs
文件使用如下内容:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.Win32;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.IO.MemoryMappedFiles;
using System.IO.Pipes;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Net.Sockets;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
[MemoryDiagnoser(displayGenColumns: false)]
[DisassemblyDiagnoser]
[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public partial class Program
{
static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
// ... copy [Benchmark]s here
}
接下来,我们可以在当前目录试着做一次项目还原,这样也许更好:
dotnet restore .
对于本文中所包含的每一个基准,你只需将代码复制并粘贴到这个测试类中,然后运行这些基准。例如,要运行一个比较.NET 6和.NET 7性能的基准,请执行。
dotnet run -c Release -f net6.0 --filter '**' --runtimes net6.0 net7.0
这个命令说:"在针对.NET 6表面区域的发布配置中构建基准,然后在.NET 6和.NET 7上运行所有的基准。" 或者只在.NET 7上运行。
dotnet run -c Release -f net7.0 --filter '**' --runtimes net7.0
而不是针对.NET 7的表面区域进行构建,然后只针对.NET 7运行一次。你可以在Windows、Linux或macOS中的任何一个系统上这样做。除非另有说明(例如改进是针对Unix的,我在Linux上运行基准),我分享的结果是在Windows 11 64位上记录的,但不是Windows特有的,在其他操作系统上也应该显示类似的相对差异。
第一个.NET 7候选版本的发布就在眼前了。这篇文章中的所有测量结果都是用最近的.NET 7 RC1的日常构建收集的。
另外,我的标准警告:这些都是微观基准测试。预计不同的硬件、不同版本的操作系统以及目前的风向都会影响相关的数字。你的里程可能会有所不同。
JIT
我想在讨论即时编译器(Just-In-Time, JIT)的性能改进时,先谈一谈一些本身并不是性能改进的东西。在对低级别的、对性能敏感的代码进行微调时,能够准确了解JIT生成的汇编代码是至关重要的。有多种方法可以获得该汇编代码。在线工具sharplab.io在这方面非常有用(感谢@ashmind提供的这个工具);但是它目前只针对一个版本,所以当我写这篇文章时,我只能看到.NET 6的输出,这使得它难以用于A/B比较。godbolt.org在这方面也很有价值,@hez2010的compiler-explorer/compiler-explorer#3168中增加了C#支持,但有类似的限制。最灵活的解决方案是在本地获取汇编代码,因为它可以比较你想要的任何版本或本地构建,以及你需要的任何配置和开关设置。
一种常见的方法是使用benchmarkdotnet
中的[DisassemblyDiagnoser]
。简单地把[DisassemblyDiagnoser]
属性放在你的测试类上:benchmarkdotnet
会找到为你的测试生成的汇编代码和它们调用的一些深度函数,并把找到的汇编代码以人类可读的形式倾倒出来。例如,如果我运行这个测试。
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
[DisassemblyDiagnoser]
public partial class Program
{
static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
private int _a = 42, _b = 84;
[Benchmark]
public int Min() => Math.Min(_a, _b);
}
并且:
dotnet run -c Release -f net7.0 --filter '**'
除了做所有正常的测试执行和计时外,benchmarkdotnet
还输出一个Program-asm.md
文件,其中包含这个:
## .NET 7.0.0 (7.0.22.37506), X64 RyuJIT AVX2
; Program.Min()
mov eax,[rcx+8]
mov edx,[rcx+0C]
cmp eax,edx
jg short M00_L01
mov edx,eax
M00_L00:
mov eax,edx
ret
M00_L01:
jmp short M00_L00
; Total bytes of code 17
未完待续...