乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 获取.Net 7并查看.Net 7中的性能提升(简中译文)

什么是.Net 7

.Net 7目前是.Net实现的最新版本,暂时还是预览阶段,已经更新到Preview 7。

image

获取.Net 7

https://dotnet.microsoft.com/zh-cn/download/dotnet/7.0

Windows

MacOS

安装.Net 7

image
image

验证安装:

dotnet --info

安装之前

image

安装之后

image

.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

image

你的新的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 .

image

对于本文中所包含的每一个基准,你只需将代码复制并粘贴到这个测试类中,然后运行这些基准。例如,要运行一个比较.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);
}

image

并且:

dotnet run -c Release -f net7.0 --filter '**'

image

image

除了做所有正常的测试执行和计时外,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

未完待续...

参考

posted @ 2022-09-01 10:18  TaylorShi  阅读(788)  评论(0编辑  收藏  举报