点击上方蓝字
关注我们
(本文阅读时间:15分钟)
接上篇内容,本篇文章将介绍:DependentHandle 现已公开、RyuJIT、即用型代码/Crossgen 2、.NET 诊断:EventPipe、SDK 的相关攻略。
DependentHandle 现已公开
该 DependentHandle 类型现在是公共的,具有以下 API 表面:
namespace System.Runtime
{public struct DependentHandle : IDisposable{public DependentHandle(object? target, object? dependent);public bool IsAllocated { get; }public object? Target { get; set; }public object? Dependent { get; set; }public (object? Target, object? Dependent) TargetAndDependent { get; }public void Dispose();}
}
它可用于创建高级系统,例如复杂的缓存系统或 ConditionalWeakTable<TKey, TValue>类型的自定义版本。例如,它将被 MVVM Toolkit 中的 WeakReferenceMessenger 类型使用,以避免在广播消息时分配内存。
▌可移植线程池
.NET 线程池已作为托管实现重新实现,现在用作 .NET 6 中的默认线程池。我们进行此更改以使所有 .NET 应用程序都可以访问同一个线程池,而不管是否正在使用 CoreCLR、Mono 或任何其他运行时。作为此更改的一部分,我们没有观察到或预期任何功能或性能影响。
RyuJIT
该团队在此版本中对 .NET JIT 编译器进行了许多改进,在每个预览帖子中都有记录。这些更改中的大多数都提高了性能。这里介绍了一些 RyuJIT 的亮点。
▌动态 PGO
在 .NET 6 中,我们启用了两种形式的 PGO(配置文件引导优化):
动态 PGO 使用从当前运行中收集的数据来优化当前运行。
静态 PGO 依靠从过去运行中收集的数据来优化未来运行。
动态 PGO 已经在文章前面的性能部分中介绍过。我将提供一个重新上限。
动态 PGO 使 JIT 能够在运行时收集有关实际用于特定应用程序运行的代码路径和类型的信息。然后,JIT 可以根据这些代码路径优化代码,有时会显着提高性能。我们在测试和生产中都看到了两位数的健康改进。有一组经典的编译器技术在没有 PGO 的情况下使用 JIT 或提前编译都无法实现。我们现在能够应用这些技术。热/冷分离是一种这样的技术,而去虚拟化是另一种技术。
要启用动态 PGO,请在应用程序将运行的环境中进行设置DOTNET_TieredPGO=1。
如性能部分所述,动态 PGO 将 TechEmpower JSON“MVC”套件每秒的请求数提高了 26%(510K -> 640K)。这是一个惊人的改进,无需更改代码。
我们的目标是在未来的 .NET 版本中默认启用动态 PGO,希望在 .NET 7 中启用。我们强烈建议您在应用程序中尝试动态 PGO 并向我们提供反馈。
▌完整的 PGO
要充分利用 Dynamic PGO,您可以设置两个额外的环境变量:DOTNET_TC_QuickJitForLoops=1和DOTNET_ReadyToRun=0. 这确保了尽可能多的方法参与分层编译。我们将此变体称为Full PGO。与动态 PGO 相比,完整 PGO 可以提供更大的稳态性能优势,但启动时间会更慢(因为必须在第 0 层运行更多方法)。
您不希望将此选项用于短期运行的无服务器应用程序,但对于长期运行的应用程序可能有意义。
在未来的版本中,我们计划精简和简化这些选项,以便您可以更简单地获得完整 PGO 的好处并用于更广泛的应用程序。
▌静态 PGO
我们目前使用静态 PGO来优化 .NET 库程序集,例如 R2R(Ready To Run)附带的程序集System.Private.CoreLib。
静态 PGO 的好处是,在使用 crossgen 将程序集编译为 R2R 格式时会进行优化。这意味着有运行时的好处而没有运行时成本。这是非常重要的,也是 PGO 对 C++ 很重要的原因。
▌循环对齐
内存对齐是现代计算中各种操作的共同要求。在 .NET 5 中,我们开始在 32 字节边界对齐方法。在 .NET 6 中,我们添加了一项执行自适应循环对齐的功能,该功能在具有循环的方法中添加NOP填充指令,以便循环代码从 mod(16) 或 mod(32) 内存地址开始。这些更改改进并稳定了 .NET 代码的性能。
在下面的冒泡排序图中,数据点 1 表示我们开始在 32 字节边界对齐方法的点。数据点 2 表示我们也开始对齐内部循环的点。如您所见,基准测试的性能和稳定性都有很大提高。
▌硬件加速结构
结构是 CLR 类型系统的重要组成部分。近年来,它们经常被用作整个 .NET 库中的性能原语。最近的例子ValueTask是ValueTuple和Span<T>。记录结构是一个新的例子。在 .NET 5 和 .NET 6 中,我们一直在提高结构的性能,部分原因是通过确保结构是局部变量、参数或方法的返回值时可以保存在超快速 CPU 寄存器中)。这对于使用向量计算的 API 特别有用。
▌稳定性能测量
团队中有大量从未出现在博客上的工程系统工作。这对于您使用的任何硬件或软件产品都是如此。JIT 团队开展了一个项目来稳定性能测量,目标是增加我们内部性能实验室自动化自动报告的回归值。这个项目很有趣,因为需要进行深入调查和产品更改才能实现稳定性。它还展示了我们为保持和提高绩效而衡量的规模。
此图像演示了不稳定的性能测量,其中性能在连续运行中在慢速和快速之间波动。x 轴是测试日期,y 轴是测试时间,以纳秒为单位。到图表末尾(提交这些更改后),您可以看到测量值稳定,结果最好。这张图片展示了一个单一的测试。还有更多测试在dotnet/runtime #43227中被证明具有类似的行为。
即用型代码/Crossgen 2
Crossgen2 是crossgen 工具的替代品。它旨在满足两个结果:
让crossgen开发更高效。
启用一组目前无法通过 crossgen 实现的功能。
这种转换有点类似于本机代码 csc.exe 到托管代码Roslyn 编译器。Crossgen2 是用 C# 编写的,但是它没有像 Roslyn 那样公开一个花哨的 API。
我们可能已经/已经为 .NET 6 和 7 计划了六个项目,这些项目依赖于 crossgen2。矢量指令默认提议是我们希望为 .NET 6 但更可能是 .NET 7 进行的 crossgen2 功能和产品更改的一个很好的例子。版本气泡是另一个很好的例子。
Crossgen2 支持跨操作系统和架构维度的交叉编译(因此称为“crossgen”)。这意味着您将能够使用单个构建机器为所有目标生成本机代码,至少与准备运行的代码相关。但是,运行和测试该代码是另一回事,为此您需要合适的硬件和操作系统。
第一步是用crossgen2编译平台本身。我们使用 .NET 6 完成了所有架构的任务。因此,我们能够在此版本中淘汰旧的 crossgen。请注意,crossgen2 仅适用于 CoreCLR,而不适用于基于 Mono 的应用程序(它们具有一组单独的代码生成工具)。
这个项目——至少一开始——并不以性能为导向。目标是启用更好的架构来托管 RyuJIT(或任何其他)编译器以离线方式生成代码(不需要或启动运行时)。
你可能会说“嘿……如果是用 C# 编写的,难道你不需要启动运行时来运行 crossgen2 吗?” 是的,但这不是本文中“离线”的含义。当 crossgen2 运行时,我们不使用运行 crossgen2 的运行时附带的 JIT 来生成准备运行 (R2R) 代码. 那是行不通的,至少对于我们的目标来说是行不通的。想象一下 crossgen2 在 x64 机器上运行,我们需要为 Arm64 生成代码。Crossgen2 将 Arm64 RyuJIT(针对 x64 编译)加载为原生插件,然后使用它生成 Arm64 R2R 代码。机器指令只是保存到文件中的字节流。它也可以在相反的方向工作。在 Arm64 上,crossgen2 可以使用编译为 Arm64 的 x64 RyuJIT 生成 x64 代码。我们使用相同的方法来针对 x64 机器上的 x64 代码。Crossgen2 会加载一个 RyuJIT,它是为任何需要的配置而构建的。这可能看起来很复杂,但如果您想启用无缝的交叉定位模型,它就是您需要的那种系统,而这正是我们想要的。
我们希望只在一个版本中使用术语“crossgen2”,之后它将替换现有的 crossgen,然后我们将回到使用术语“crossgen”来表示“crossgen2”。
.NET 诊断:EventPipe
EventPipe 是我们用于在进程内或进程外输出事件、性能数据和计数器的跨平台机制。从 .NET 6 开始,我们已将实现从 C++ 移至 C。通过此更改,Mono 也使用 EventPipe。这意味着 CoreCLR 和 Mono 都使用相同的事件基础设施,包括 .NET 诊断 CLI 工具。
这一变化还伴随着 CoreCLR 的小幅减小:
库 | 大小之后-大小之前 | 差异 |
libcoreclr.so | 7037856 – 7049408 | -11552 |
我们还进行了一些更改,以提高 EventPipe 在负载下的吞吐量。在最初的几个预览版中,我们进行了一系列更改,从而使吞吐量提高了 .NET 5 的 2.06 倍:
对于这个基准,越高越好。.NET 6 是橙色线,.NET 5 是蓝色线。
SDK
对 .NET SDK 进行了以下改进。
▌.NET 6 SDK 可选工作负载的 CLI 安装
.NET 6 引入了SDK 工作负载的概念。工作负载是可选组件,可以安装在 .NET SDK 之上以启用各种场景。.NET 6 中的新工作负载是:.NET MAUI 和 Blazor WebAssembly AOT 工作负载。我们可能会在 .NET 7 中创建新的工作负载(可能来自现有的 SDK)。工作负载的最大好处是减少大小和可选性。我们希望随着时间的推移使 SDK 变得更小,并且只安装您需要的组件。这个模型对开发者机器有好处,对 CI 来说甚至更好。
Visual Studio 用户并不真正需要担心工作负载。工作负载功能经过专门设计,以便像 Visual Studio 这样的安装协调器可以为您安装工作负载。可以通过 CLI 直接管理工作负载。
工作负载功能公开了用于管理工作负载的多个动词,包括以下几个:
dotnet workload restore— 安装给定项目所需的工作负载。
dotnet workload install— 安装命名工作负载。
dotnet workload list— 列出您已安装的工作负载。
dotnet workload update— 将所有已安装的工作负载更新到最新的可用版本。
update 动词查询更新 nuget.org的工作负载清单、更新本地清单、下载已安装工作负载的新版本,然后删除所有旧版本的工作负载。这类似于 apt update && apt upgrade -y(用于基于 Debian 的 Linux 发行版)。将工作负载视为 SDK 的私有包管理器是合理的。它是私有的,因为它仅适用于 SDK 组件。我们将来可能会重新考虑这一点。这些 dotnet workload 命令在给定 SDK 的上下文中运行。假设您同时安装了 .NET 6 和 .NET 7。工作负载命令将为每个 SDK 提供不同的结果,因为工作负载将不同(至少相同工作负载的不同版本)。
请注意,将 NuGet.org 中的工作负载复制到您的 SDK 安装中,因此如果 SDK 安装位置受到保护(即在管理员/根位置),dotnet workload install则需要运行提升或使用sudo。
▌内置 SDK 版本检查
为了更容易跟踪 SDK 和运行时的新版本何时可用,我们向 .NET 6 SDK 添加了一个新命令。
dotnet sdk check
它会告诉您是否有可用于您已安装的任何 .NET SDK、运行时或工作负载的更新版本。您可以在下图中看到新体验。
dotnet new
您现在可以在 NuGet.org 中搜索带有.dotnet new --search
模板安装的其他改进包括支持切换以支持私 有NuGet 源的授权凭据。--interactive
安装 CLI 模板后,您可以通过和检查更新是否可用。--update-check--update-apply
▌NuGet 包验证
包验证工具使 NuGet 库开发人员能够验证他们的包是否一致且格式正确。
这包括:
验证版本之间没有重大更改。
验证包对于所有特定于运行时的实现是否具有相同的公共 API 集。
确定任何目标框架或运行时适用性差距。
该工具是 SDK 的一部分。使用它的最简单方法是在项目文件中设置一个新属性。
<EnablePackageValidation> true </EnablePackageValidation>
▌更多 Roslyn 分析仪
在 .NET 5 中,我们提供了大约 250 个带有 .NET SDK 的分析器。其中许多已经存在,但作为 NuGet 包在带外发送。我们为 .NET 6 添加了更多分析器。
默认情况下,大多数新分析器都在信息级别启用。您可以通过如下配置分析模式在警告级别启用这些分析器:<AnalysisMode>All</AnalysisMode>
我们为 .NET 6 发布了我们想要的一组分析器(加上一些附加功能),然后将它们中的大多数做成了可供抓取的。社区添加了几个实现,包括这些。
贡献者 | 问题 | 标题 |
纽厄尔·克拉克 | dotnet/运行时 #33777 | 使用基于跨度的string.Concat |
纽厄尔·克拉克 | dotnet/运行时 #33784 | 解析时优先string.AsSpan()string.Substring() |
纽厄尔·克拉克 | dotnet/运行时 #33789 | 覆盖Stream.ReadAsync/WriteAsync |
纽厄尔·克拉克 | dotnet/运行时 #35343 | 替换为Dictionary<,>.Keys.ContainsContainsKey |
纽厄尔·克拉克 | dotnet/运行时 #45552 | 使用代替String.EqualsString.Compare |
梅克特雷尔 | dotnet/运行时 #47180 | 使用代替String.Contains(char)String.Contains(String) |
感谢 Meik Tranel 和 Newell Clark。
▌为 Platform Compatibility Analyzer 启用自定义防护
CA1416 平台兼容性分析器已经使用 OperatingSystem 和 RuntimeInformation 中的方法识别平台防护,例如 OperatingSystem.IsWindows 和OperatingSystem.IsWindowsVersionAtLeast。但是,分析器无法识别任何其他保护可能性,例如缓存在字段或属性中的平台检查结果,或者在辅助方法中定义了复杂的平台检查逻辑。
为了允许自定义守卫的可能性,我们添加了新属性 SupportedOSPlatformGuard并UnsupportedOSPlatformGuard 使用相应的平台名称和/或版本注释自定义守卫成员。此注释被平台兼容性分析器的流分析逻辑识别和尊重。
▌用法
[UnsupportedOSPlatformGuard("browser")] // The platform guard attribute
#if TARGET_BROWSERinternal bool IsSupported => false;
#elseinternal bool IsSupported => true;
#endif[UnsupportedOSPlatform("browser")]void ApiNotSupportedOnBrowser() { }void M1(){ApiNotSupportedOnBrowser(); // Warns: This call site is reachable on all platforms.'ApiNotSupportedOnBrowser()' is unsupported on: 'browser'if (IsSupported){ApiNotSupportedOnBrowser(); // Not warn}}[SupportedOSPlatform("Windows")][SupportedOSPlatform("Linux")]void ApiOnlyWorkOnWindowsLinux() { }[SupportedOSPlatformGuard("Linux")][SupportedOSPlatformGuard("Windows")]private readonly bool _isWindowOrLinux = OperatingSystem.IsLinux() || OperatingSystem.IsWindows();void M2(){ApiOnlyWorkOnWindowsLinux(); // This call site is reachable on all platforms.'ApiOnlyWorkOnWindowsLinux()' is only supported on: 'Linux', 'Windows'.if (_isWindowOrLinux){ApiOnlyWorkOnWindowsLinux(); // Not warn}}
}
结束
欢迎使用 .NET 6。它是另一个巨大的 .NET 版本,在性能、功能、可用性和安全性方面都有很多的改进。我们希望您能找到许多改进,最终使您在日常开发中更有效率和能力,并提高性能或降低生产中应用程序的成本。我们已经开始从那些已经开始使用 .NET 6 的人那里听到好消息。
在 Microsoft,我们还处于 .NET 6 部署的早期阶段,一些关键应用程序已经投入生产,未来几周和几个月内还会有更多应用程序推出。
.NET 6 是我们最新的 LTS 版本。我们鼓励每个人都转向它,特别是如果您使用的是 .NET 5。我们期待它成为有史以来采用速度最快的 .NET 版本。
此版本是至少 1000 人(但可能更多)的结果。这包括来自 Microsoft 的 .NET 团队以及社区中的更多人。我试图在这篇文章中包含许多社区贡献的功能。感谢您抽出宝贵时间创建这些内容并完成我们的流程。我希望这次经历是一次美好的经历,并且更多的人会做出贡献。
感谢您成为 .NET 开发人员。
往期精彩回顾
▎.NET 6 攻略大全(一)
▎.NET 6 攻略大全(二)
了解更多.NET