...

宣布 .NET 6 — 迄今为止最快的 .NET

2021-12-13


2021 年 11 月 8 日 (以下是译文)


欢迎使用 .NET 6。今天的发布是 .NET 团队和社区一年多来努力的结果。C# 10 和 F# 6 提供了语言改进,使您的代码更简单、更好。性能有了巨大的提升,我们已经看到降低了 Microsoft 托管云服务的成本。.NET 6 是第一个原生支持 Apple Silicon (Arm64) 的版本,并且还针对 Windows Arm64 进行了改进。我们构建了一个新的动态配置文件引导优化 (PGO) 系统,该系统可提供仅在运行时才可能进行的深度优化。使用dotnet monitorOpenTelemetry改进了云诊断。WebAssembly支持更强大、更高效。添加了新的 API,用于HTTP/3处理 JSON数学,直接操作内存。.NET 6 将得到三年支持。开发人员已经开始将应用程序升级到 .NET 6,我们已经在生产中听到了很好的早期结果。.NET 6 已为您的应用做好准备。

您可以下载适用于 Linux、macOS 和 Windows 的.NET 6

有关Web 方案的新增功能,请参阅ASP.NET Core帖子。

Visual Studio 2022 也在今天发布。阅读公告观看发布活动以了解有关发布的更多信息。

PowerShell 7.2也在今天发布,基于 .NET 6。PowerShell 用户可以访问与 .NET 开发人员相同的性能改进和 API。

.NET Conf是一个为期三天的免费虚拟开发人员活动,旨在庆祝 .NET 的主要版本。它将于明天开始,并于 11 月 9 日至 11 日举行,届时将有来自我们团队、Microsoft 团队和更广泛社区的演讲者参加 80 多场会议。收听学习并与我们互动

查看新的对话帖子,就最新的 .NET 功能进行工程师对工程师的深入讨论。

.NET 6 亮点

.NET 6 是:

该版本包括大约一万个 git 提交。即使这篇文章很长,它也跳过了许多改进。您必须下载并试用 .NET 6 才能看到所有新内容。

支持

.NET 6 是一个长期支持 (LTS) 版本,将支持三年。它支持多种操作系统,包括 macOS Apple Silicon 和 Windows Arm64。

红帽与 .NET 团队合作红帽企业 Linux 上支持 .NET。在 RHEL 8 及更高版本上,.NET 6 将可用于 AMD 和 Intel (x64_64)、ARM (aarch64) 以及 IBM Z 和 LinuxONE (s390x) 架构。

请开始将您的应用程序迁移到 .NET 6,尤其是 .NET 5 应用程序。我们从早期采用者那里听说,从 .NET Core 3.1 和 .NET 5 升级到 .NET 6 很简单。

Visual Studio 2022Visual Studio 2022 for Mac支持 .NET 6 。Visual Studio 2019、Visual Studio for Mac 8 或 MSBuild 16 不支持它。如果要使用 .NET 6,则需要升级到Visual Studio 2022(现在也是 64 位)。Visual Studio CodeC# 扩展支持 .NET 6 。

Azure 应用服务:

注意:如果您的应用已在应用服务上运行 .NET 6 预览版或 RC 构建,则一旦 .NET 6 运行时和 SDK 部署到您所在的区域,它将在第一次重新启动时自动更新。如果您部署了自包含应用程序,则需要重新构建和重新部署。

统一扩展平台

.NET 6 为浏览器桌面IoT移动应用程序提供了一个统一的平台。底层平台已更新,以满足所有应用程序类型的需求,并使您可以轻松地在所有应用程序中重用代码。新功能和改进可同时用于所有应用程序,因此您在云中或移动设备上运行的代码具有相同的行为方式并具有相同的优势。





随着每个版本的发布,.NET 开发人员的影响范围不断扩大。机器学习WebAssembly是最近添加的两个。例如,通过机器学习,您可以编写应用程序来查找流数据中的异常情况。使用 WebAssembly,您可以在浏览器中托管 .NET 应用程序,就像 HTML 和 JavaScript 一样,或者将它们与 HTML 和 JavaScript 混合使用

最令人兴奋的新增功能之一是.NET 多平台应用程序 UI (.NET MAUI)。您现在可以在单个项目中编写代码,从而提供跨桌面和移动操作系统的现代客户端应用程序体验。.NET MAUI 的发布时间将比 .NET 6 晚一点。我们在 .NET MAUI 上投入了大量时间和精力,很高兴能够发布它并看到 .NET MAUI 应用程序投入生产。

当然,.NET 应用程序也可以在Windows 桌面上使用(使用Windows 窗体WPF)以及在云中使用http://ASP.NETCore。它们是我们提供时间最长的应用程序类型,并且仍然非常受欢迎,我们在 .NET 6 中对它们进行了改进。

面向 .NET 6

继续以广泛的平台为主题,在所有这些操作系统上编写 .NET 代码很容易。

以 .NET 6目标,您需要使用 .NET 6 目标框架,如下所示:

net6.0

该目标框架名字对象(TFM),您可以访问所有的跨平台的API,.NET提供。如果您正在编写控制台应用程序、http://ASP.NETCore 应用程序或可重用的跨平台库,这是最佳选择。net6.0

如果您的目标是特定的操作系统(例如编写Windows 窗体或 iOS 应用程序),那么还有另一组 TFM(每个都针对一个不言而喻的操作系统)供您使用。它们使您可以访问所有 API以及一系列特定于操作系统的 API 。net6.0

  • net6.0-android

  • net6.0-ios

  • net6.0-maccatalyst

  • net6.0-tvos

  • net6.0-windows

每个无版本的 TFM 都相当于针对 .NET 6 支持的最低操作系统版本。如果您想要特定或访问更新的 API,可以指定操作系统版本。

的和TFMS支持(同.NET 5)。Android 和 Apple TFM 是 .NET 6 的新增功能,目前处于预览阶段。稍后的 .NET 6 更新将支持它们。net6.0net6.0-windows

操作系统特定的 TFM 之间没有兼容性关系。例如,与. 如果您想共享代码,您需要使用带有语句的源代码或带有目标代码的二进制文件来实现。net6.0-iosnet6.0-tvos#ifnet6.0

表现

自从我们启动 .NET Core 项目以来,该团队就一直非常关注性能。Stephen Toub在捕捉每个版本的 .NET 性能进展方面做得非常出色。如果您还没有机会,我建议您查看他在 .NET 6 中的性能改进帖子。

在这篇博文中,我收集了一些您想了解的重大性能改进,包括文件 IO、界面转换、PGO 和 System.Text.Json。

动态 PGO

动态轮廓引导优化 (PGO)可以显着提高稳态性能。例如,PGO 使 TechEmpower JSON“MVC”套件的每秒请求数提高了 26%(510K -> 640K)。

动态 PGO 建立在分层编译之上,它使方法能够首先非常快速地编译(称为“第 0 层”)以提高启动性能,然后在启用大量优化的情况下随后重新编译(称为“第 1 层”)一旦这种方法被证明是有效的。该模型使方法能够在第 0 层中进行检测,以允许对代码的执行进行各种观察。当这些方法在第 1 层重新编译时,从第 0 层执行中收集的信息将用于更好地优化第 1 层代码。这就是机制的本质。

动态 PGO 的启动时间将比默认运行时稍慢,因为在第 0 层方法中运行额外的代码来观察方法行为。

要启用动态 PGO,请在您的应用程序将运行的环境中进行设置。您还必须确保启用分层编译(默认情况下)。动态 PGO 是可选的,因为它是一种新的、有影响力的技术。我们希望发布选择性使用和相关反馈,以确保它经过全面压力测试。我们对分层编译做了同样的事情。至少一个非常大的 Microsoft 服务支持动态 PGO,并且已经在生产中使用它。我们鼓励您尝试一下。DOTNET_TieredPGO=1

您可以在 .NET 6中的性能博文中看到更多关于动态 PGO 优势的信息,包括以下微基准测试,它测量特定 LINQ 枚举器的成本。

private IEnumerator _source = Enumerable.Range(0, long.MaxValue).GetEnumerator();

[Benchmark]
public void MoveNext() => _source.MoveNext();

这是有和没有动态 PGO 的结果。




这是一个相当大的差异,但也增加了代码大小,这可能会让一些读者感到惊讶。这是 JIT 生成的汇编代码的大小,而不是内存分配(这是一个更常见的焦点)。.NET 6 Performance 帖子对此有很好的解释。

PGO 实现中常见的一种优化是“热/冷拆分”,其中经常执行的方法部分(“热”)在方法开始时靠近在一起,而不经常执行的方法部分(“冷”)被移到一起移动到方法的末尾。这可以更好地使用指令缓存并最大限度地减少可能未使用的代码的负载。

作为上下文,接口调度是 .NET 中最昂贵的调用类型。非虚拟方法调用是最快的,甚至更快的是可以通过内联消除的调用。在这种情况下,动态 PGO 为MoveNext. 第一个 — 热的 — 是直接调用,另一个 — 冷的 — 是通过. 如果最热门的人大部分时间都被跟注,那将是一场巨大的胜利。Enumerable+RangeIterator.MoveNextIEnumerator

这就是魔法。当 JIT 检测此方法的第 0 层代码时,包括检测此接口分派以跟踪_source每次调用的具体类型。并且 JIT 发现每次调用都在一个名为 的类型上,这是一个用于在实现内部实现的私有类。因此,对于第 1 层,JIT 已发出检查以查看类型是否为:如果不是,则它跳转到我们之前强调的执行正常接口调度的冷部分。但如果是——基于分析数据预计在绝大多数时间都是这种情况——然后它可以继续直接调用Enumerable+RangeIteratorEnumerable.RangeEnumerable_sourceEnumerable+RangeIteratorEnumerable+RangeIterator.MoveNext方法,非虚拟化。不仅如此,它还认为内联该MoveNext方法是有利可图的。最终效果是生成的汇编代码稍大一些,但针对预期最常见的确切场景进行了优化。当我们开始构建动态 PGO 时,这些就是我们想要的胜利。

动态 PGO 在 RyuJIT 部分再次讨论。

文件 IO 改进

FileStream几乎完全用 .NET 6 重写,重点是提高异步文件 IO 性能。在 Windows 上,实现不再使用阻塞 API 并且可以快几倍!我们还改进了所有平台上的内存使用。在第一个异步操作(通常分配)之后,我们已经使异步操作无分配!此外,当 Windows 和 Unix 实现不同(并且这是可能的)时,我们使边缘情况的行为变得统一。

这种重写的性能改进使所有操作系统受益。Windows 的好处是最高的,因为它远远落后。macOS 和 Linux 用户还应该看到显着的FileStream性能改进。

以下基准测试将 100 MB 写入新文件。

private byte[] _bytes = new byte[8_000];

[Benchmark]
public async Task Write100MBAsync()
{
    using FileStream fs = new("file.txt", FileMode.Create, FileAccess.Write, FileShare.None, 1, FileOptions.Asynchronous);
    for (int i = 0; i < 100_000_000 / 8_000; i++)
        await fs.WriteAsync(_bytes);
}

在带有 SSD 驱动器的 Windows 上,我们观察到了4 倍的加速和超过1200 倍的分配下降:

我们还认识到需要更多高性能文件 IO 功能:并发读写和分散/聚集 IO。针对这些情况,我们为和类引入了新的 API 。System.IO.FileSystem.IO.RandomAccess

async Task AllOrNothingAsync(string path, IReadOnlyList buffers)
{
    using SafeFileHandle handle = File.OpenHandle(
        path, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.Asynchronous,
        preallocationSize: buffers.Sum(buffer => buffer.Length)); // hint for the OS to pre-allocate disk space

    await RandomAccess.WriteAsync(handle, buffers, fileOffset: 0); // on Linux it's translated to a single sys-call!
}

示例演示:

预分配大小功能提高了性能,因为写入操作不需要扩展文件,而且文件碎片化的可能性较小。这种方法提高了可靠性,因为写操作将不再因空间不足而失败,因为空间已经被保留。Scatter/Gather IO API 减少了写入数据所需的系统调用次数。

更快的界面检查和转换

界面投射性能提升了 16% – 38%。这种改进对于 C# 与接口之间的模式匹配特别有用。





该图表展示了代表性基准的改进规模。

将 .NET 运行时的一部分从 C++ 迁移到托管 C# 的最大优势之一是它降低了贡献的障碍。这包括接口转换,它作为 .NET 6 的早期更改移至 C#。.NET 生态系统中通晓 C# 的人多于 C++(并且运行时使用具有挑战性的 C++ 模式)。能够阅读组成运行时的一些代码是培养对以各种形式贡献的信心的重要一步。

归功于本·亚当斯

System.Text.Json 源代码生成器

我们为 System.Text.Json添加了一个源代码生成器,它避免了在运行时进行反射和代码生成的需要,并且可以在构建时生成最佳序列化代码。序列化程序通常使用非常保守的技术编写,因为它们必须如此。但是,如果您阅读自己的序列化源代码(使用序列化程序),您会看到哪些明显的选择可以使序列化程序在您的特定情况下更加优化。这正是这个新的源生成器所做的。

除了提高性能和减少内存之外,源代码生成器还可以生成最适合程序集修整的代码。这有助于开发更小的应用程序。

序列化POCO是一个非常常见的场景。使用新源发生器,我们观察到,序列化是〜1.6倍快与我们的基准

TechEmpower缓存基准行使平台或从数据库来源的信息架构的内存缓存。基准测试的 .NET 实现对缓存数据执行 JSON 序列化,以便将其作为对测试工具的响应发送。

我们观察到~100K RPS 增益(~40% 增加)。与MemoryCache 性能改进相结合时,.NET 6 的吞吐量比 .NET 5 高 50% !

C# 10

欢迎使用 C# 10。C# 10 的一个主要主题是继续从C# 9 中的顶级语句开始的简化之旅。新功能从 中删除了更多的仪式,导致程序短至一行。他们的灵感来自与没有 C# 经验的人(学生、专业开发人员和其他人)交谈,并学习对他们来说最有效且直观的方法。Program.cs

大多数.NET SDK 模板已经更新,以提供 C# 10 现在可以实现的更简单、更简洁的体验。我们听到反馈说有些人不喜欢新模板,因为它们不是为专家设计的,删除面向对象,删除在编写 C# 的第一天就需要学习的重要概念,或鼓励在一个文件中编写整个程序。客观地说,这些观点都不是真的。新模型同样适用于学生和专业开发人员。然而,它与我们在 .NET 6 之前拥有的 C 派生模型不同。

C# 10 中还有其他一些功能和改进,包括记录结构。

全局 using 指令

全局 using 指令让您using只需指定一次指令,并将其应用于您编译的每个文件。

以下示例显示了语法的广度:

  • global using System;

  • global using static System.Console;

  • global using Env = System.Environment;

您可以将语句放在任何文件中,包括在.global using.csProgram.cs

隐式 using 是一个 MSBuild 概念,它根据 SDK自动添加一组指令。例如,控制台应用隐式使用不同于http://ASP.NETCore。global using

隐式使用是选择加入的,并在以下位置启用PropertyGroup:

  • enable

隐式使用是现有项目的选择加入,但默认情况下包含在新的 C# 项目中。有关更多信息,请参阅隐式使用

文件范围的命名空间

文件范围的命名空间使您可以为整个文件声明命名空间,而无需将其余内容嵌套在. 只允许一个,并且它必须出现在声明任何类型之前。{ ... }

新语法是一行:

namespace MyNamespace;

class MyClass { ... } // Not indented

这种新语法是三行缩进样式的替代:

namespace MyNamespace
{
    class MyClass { ... } // Everything is indented
}

好处是在整个文件都在同一个命名空间中的极其常见的情况下减少缩进。

记录结构

C# 9 引入了记录作为类的一种特殊的面向值的形式。在 C# 10 中,您还可以声明结构记录。C# 中的结构已经具有值相等性,但记录结构添加了一个==运算符和一个 的实现,以及一个基于值的实现:IEquatableToString

public record struct Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

就像记录类一样,记录结构可以是“位置”的,这意味着它们有一个主构造函数,它隐式声明了与参数对应的公共成员:

public record struct Person(string FirstName, string LastName);

但是,与记录类不同,隐式公共成员是可变的自动实现的属性。这是因为记录结构是元组的自然成长故事。例如,如果您有一个返回类型,并且您想将其扩展为命名类型,您可以轻松声明相应的位置结构记录并维护可变语义。(string FirstName, string LastName)

如果您想要一个具有只读属性的不可变记录,您可以声明整个记录结构readonly(就像其他结构一样):

public readonly record struct Person(string FirstName, string LastName);

C# 10 不仅支持with记录结构的表达式,还支持所有结构以及匿名类型的表达式:

var updatedPerson = person with { FirstName = "Mary" };

F# 6

F# 6旨在让 F# 更简单、更高效。这适用于语言设计、库和工具。我们对 F# 6(及更高版本)的目标是消除语言中让用户感到惊讶或对学习 F# 造成障碍的极端情况。我们很高兴与 F# 社区合作进行这项持续的工作。

使 F# 更快、更具互操作性

新语法直接创建一个任务并启动它。这是 F# 6 中最重要的特性之一,它使异步任务更简单、性能更高,并且与 C# 和其他 .NET 语言的互操作性更强。以前,创建 .NET 任务需要使用创建任务和调用.task {…}async {…}Async.StartImmediateAsTask

该功能建立在称为“可恢复代码”RFC FS-1087的基础之上。可恢复代码是一个核心特性,我们希望在未来使用它来构建其他高性能异步和屈服状态机。task {…}

F# 6 还为库作者添加了其他性能特性,包括InlineIfLambdaF# 活动模式的未装箱表示。一个特别显着的性能改进是在列表和数组表达式的编译中,现在它们的速度提高了4 倍,并且调试也更好更简单。

使 F# 更易于学习且更统一

F# 6 启用索引语法。到目前为止,F# 一直使用 expr.[idx] 进行索引。删除点符号是基于第一次使用 F# 用户的反复反馈,即使用点是与他们期望的标准实践的不必要的分歧。在新代码中,我们建议系统地使用新的索引语法。作为一个社区,我们都应该改用这种语法。expr[idx]expr[idx]

F# 社区做出了重要改进,使 F# 语言在 F# 6 中更加统一。其中最重要的是消除了 F# 缩进规则中的许多不一致和限制。其他使 F# 更加统一的设计添加包括添加as模式;在计算表达式中允许“重载自定义操作”(对 DSL 有用);允许_丢弃use绑定并允许输出中的二进制格式。F# 核心库添加了用于对列表、数组和序列进行复制和更新的新函数,以及其他内在函数。从 2.0 开始不推荐使用的 F# 的一些遗留功能现在会导致错误。其中许多更改更好地使 F# 符合您的期望,从而减少了意外。%BNativePtr

F# 6 还添加了对 F# 中其他“隐式”和“类型导向”转换的支持。这意味着更少的显式向上转换,并增加了对 .NET 样式隐式转换的一流支持。F# 还进行了调整,以更好地适应使用 64 位整数的数字库时代,并对 32 位整数进行隐式加宽。

改进 F# 工具

F# 6 中的工具改进使日常编码变得更容易。新的“管道调试”允许您单步执行、设置断点并检查 F# 管道语法的中间值。阴影值的调试显示已得到改进,消除了调试时常见的混淆源。现在,F# 工具的性能也更高,F# 编译器并行执行解析阶段。F# IDE 工具也得到了改进。F# 脚本现在更加强大,允许您通过文件固定使用的 .NET SDK 版本。input |> f1 |> f2global.json

热重载

热重载是另一个性能特性,专注于开发人员的生产力。它使您能够对正在运行的应用程序进行各种代码编辑,从而减少您等待应用程序重新构建、重新启动或重新导航到进行代码更改后所在位置所需的时间。

热重载可通过dotnet watchCLI 工具和 Visual Studio 2022 使用。您可以将热重载用于多种应用程序类型,例如 ASP.NET Core、Blazor、.NET MAUI、控制台、Windows 窗体 (WinForms)、WPF、WinUI 3、Azure 函数等。

使用 CLI 时,只需使用 启动您的 .NET 6 应用程序dotnet watch,进行任何支持的编辑,并在保存文件时(如在 Visual Studio Code 中)将立即应用这些更改。如果不支持更改,详细信息将记录到命令窗口。





此图像显示正在启动的 MVC 应用程序dotnet watch。我对和文件进行了编辑(如日志中所报告的那样),并且两者都被应用到代码中并在不到半秒的时间内很快地反映在浏览器中。.cs.cshtml

使用 Visual Studio 2022 时,只需启动您的应用程序,进行受支持的更改,然后使用新的“热重载”按钮(如下图所示)应用这些更改。您还可以选择通过同一按钮上的下拉菜单在保存时应用更改。使用 Visual Studio 2022 时,Hot Reload 可用于多个 .NET 版本、.NET 5+、.NET Core 和 .NET Framework。例如,您将能够对OnClickEvent按钮的处理程序进行代码隐藏更改。Main应用程序的方法不支持它。





注意:RuntimeInformation.FrameworkDescription中存在一个错误,错误在该图像中显示,将很快修复。

热重载还与现有的“编辑并继续”功能(在断点处停止时)和 XAML 热重载协同工作,用于实时编辑应用程序 UI。目前支持 C# 和 Visual Basic 应用程序(不是 F#)。

安全

.NET 6 中的安全性得到了显着改善。它始终是团队的重要关注点,包括威胁建模、加密和纵深防御缓解措施。

在 Linux 上,我们依赖OpenSSL进行所有加密操作,包括 TLS(HTTPS 需要)。在 macOS 和 Windows 上,我们依赖操作系统提供的功能来实现相同的目的。对于 .NET 的每个新版本,我们经常需要添加对新版本 OpenSSL 的支持。.NET 6 添加了对OpenSSL 3 的支持。

OpenSSL 3 的最大变化是改进的FIPS 140-2模块和更简单的许可。

.NET 6 需要 OpenSSL 1.1 或更高版本,并且更喜欢它可以找到的最高安装版本的 OpenSSL,直到并包括 v3。在一般情况下,当您使用的 Linux 发行版切换为默认设置时,您最有可能开始使用 OpenSSL 3。大多数发行版还没有这样做。例如,如果您在 Red Hat 8 或 Ubuntu 20.04 上安装 .NET 6,您将不会(在撰写本文时)开始使用 OpenSSL 3。

OpenSSL 3、Windows 10 21H1 和 Windows Server 2022 都支持ChaCha20Poly1305。您可以在 .NET 6 中使用这种新的经过身份验证的加密方案(假设您的环境支持它)。

感谢凯文-琼斯为ChaCha20Poly1305 Linux支持。

我们还发布了新的运行时安全缓解路线图。重要的是您使用的运行时不受教科书式攻击类型的影响。我们正在满足这种需求。在 .NET 6 中,我们构建了W^X英特尔控制流执行技术 (CET) 的初始实现。W^X 完全受支持,默认情况下为 macOS Arm64 启用,并在其他环境中选择加入。CET 是所有环境的选择和预览。我们希望默认情况下为 .NET 7 中的所有环境启用这两种技术。

ARM64

如今,对于笔记本电脑、云硬件和其他设备,Arm64 令人兴奋不已。我们对 .NET 团队也感到同样的兴奋,并正在尽最大努力跟上这一行业趋势。我们直接与 Arm Holdings、Apple 和 Microsoft 的工程师合作,以确保我们的实施正确且经过优化,并且我们的计划保持一致。这些密切的伙伴关系对我们帮助很大。

  • 特别感谢 Apple 在 M1 芯片发布之前向我们的团队发送了一蒲式耳的 Arm64 开发套件,并提供了重要的技术支持。

  • 特别感谢 Arm Holdings,他们的工程师代码审查了我们的 Arm64 更改并进行了性能改进。

在此之前,我们通过 .NET Core 3.0 和 Arm32 添加了对 Arm64 的初始支持。该团队在最近的几个版本中都对 Arm64 进行了重大投资,并且在可预见的未来还将继续。在 .NET 6 中,我们的主要重点是在macOS 和 Windows Arm64 操作系统上支持新的 Apple Silicon 芯片和x64 仿真场景

您可以在 macOS 11+ 和 Windows 11+ Arm64 操作系统上安装 Arm64 和 x64 版本的 .NET。我们必须做出多项设计选择和产品更改以确保有效。

我们的策略是“亲原生架构”。我们建议您始终使用与原生架构匹配的 SDK,即 macOS 和 Windows Arm64 上的 Arm64 SDK。SDK 是一个庞大的软件体。与仿真相比,在 Arm64 芯片上本地运行的性能要高得多。我们已更新 CLI 以简化此操作。我们永远不会专注于优化模拟 x64。

默认情况下,如果您dotnet run使用 Arm64 SDK 的 .NET 6 应用程序,它将作为 Arm64 运行。您可以使用参数轻松切换到以 x64 运行,例如. 相同的论点适用于其他 CLI 动词。有关详细信息,请参阅适用于 macOS 和 Windows Arm64 的 .NET 6 RC2 更新。-adotnet run -a x64

我想确保涵盖了一个微妙之处。当您使用 时,SDK 仍以 Arm64 的形式在本机运行。.NET SDK体系结构中存在进程边界存在的固定点。大多数情况下,一个进程必须全是 Arm64 或全是 x64。我稍微简化了一点,但 .NET CLI 会等待 SDK 架构中的最后一个进程创建,然后将它作为您请求的芯片架构启动,例如 x64。这就是您的代码运行的过程。这样,您作为开发人员可以获得 Arm64 的好处,但您的代码可以在它需要的过程中运行。这仅在您需要以 x64 格式运行某些代码时才相关。如果你不这样做,那么你可以一直以 Arm64 的方式运行一切,这很好。-a x64

Arm64 支持

以下是您需要了解的关键点,适用于 macOS 和 Windows Arm64:

  • 支持并推荐 .NET 6 Arm64 和 x64 SDK。

  • 支持所有支持的 Arm64 和 x64 运行时。

  • .NET Core 3.1 和 .NET 5 SDK 可以工作,但提供的功能较少,并且在某些情况下不完全受支持。

  • dotnet test尚不能与 x64 仿真一起正常工作。我们正在为此努力。dotnet test将作为 6.0.200 版本的一部分进行改进,可能更早。

有关更完整的信息,请参阅.NET 对 macOS 和 Windows Arm64 的支持

本次讨论中缺少 Linux。它不像 macOS 和 Windows 那样支持 x64 仿真。因此,这些新的 CLI 特性和支持方法并不直接适用于 Linux,Linux 也不需要它们。

Windows  Arm64

我们有一个简单的工具来演示.NET 运行的环境

C:Usersrich>dotnet tool install -g dotnet-runtimeinfo
You can invoke the tool using the following command: dotnet-runtimeinfo
Tool 'dotnet-runtimeinfo' (version '1.0.5') was successfully installed.

C:Usersrich>dotnet runtimeinfo
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

**.NET information
Version: 6.0.0
FrameworkDescription: .NET 6.0.0-rtm.21522.10
Libraries version: 6.0.0-rtm.21522.10
Libraries hash: 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6

**Environment information
ProcessorCount: 8
OSArchitecture: Arm64
OSDescription: Microsoft Windows 10.0.22494
OSVersion: Microsoft Windows NT 10.0.22494.0

如您所见,该工具在 Windows Arm64 上本机运行。我将向您展示http://ASP.NETCore 的外观。





macOS Arm64

并且您可以看到在 macOS Arm64 上的体验是相似的,并且还展示了架构定位。

rich@MacBook-Air app % dotnet --version
6.0.100
rich@MacBook-Air app % dotnet --info | grep RID
 RID:         osx-arm64
rich@MacBook-Air app % cat Program.cs 
using System.Runtime.InteropServices;
using static System.Console;

WriteLine($"Hello, {RuntimeInformation.OSArchitecture} from {RuntimeInformation.FrameworkDescription}!");
rich@MacBook-Air app % dotnet run
Hello, Arm64 from .NET 6.0.0-rtm.21522.10!
rich@MacBook-Air app % dotnet run -a x64
Hello, X64 from .NET 6.0.0-rtm.21522.10!
rich@MacBook-Air app %

该图展示了 Arm64 执行是 Arm64 SDK 的默认执行,以及使用参数在面向 Arm64 和 x64 之间切换是多么容易。完全相同的体验适用于 Windows Arm64。-a





此图像演示了相同的内容,但使用http://ASP.NETCore。我使用的 .NET 6 Arm64 SDK 与您在上图中看到的相同。

Arm64 上的 Docker

Docker 支持在本机架构和仿真中运行的容器,本机架构是默认的。这看起来很明显,但当大多数 Docker Hub 目录面向 x64 时可能会令人困惑。您可以使用来请求 x64 图像。--platform linux/amd64

我们仅支持在 Arm64 操作系统上运行 Linux Arm64 .NET 容器映像。这是因为我们从来不支持在QEMU 中运行 .NET ,这是 Docker 用于架构模拟的。看来这可能是由于 QEMU 的限制





此图片展示了我们维护控制台例子:。这是一个有趣的示例,因为它包含一些用于打印 CPU 和内存限制信息的基本逻辑,您可以使用它们。我展示的图像设置了 CPU 和内存限制。mcr.microsoft.com/dotnet/samples

自己试试: docker run --rm mcr.microsoft.com/dotnet/samples

Arm64 性能

Apple Silicon 和 x64 仿真支持项目非常重要,但是,我们也普遍提高了 Arm64 性能。





此图展示了将堆栈帧的内容清零方面的改进,这是一种常见操作。绿线是新行为,而橙色线是另一个(不太有益的)实验,两者都相对于基线有所改进,由蓝线表示。对于这个测试,越低越好。

容器

.NET 6 更适合容器,主要基于本文中讨论的所有改进,适用于 Arm64 和 x64。我们还进行了关键更改,这将有助于各种场景。使用 .NET 6 验证容器改进演示了其中一些改进正在一起测试。

Windows 容器改进和新的环境变量也已包含在 11 月 9 日(明天)发布的 11 月 .NET Framework 4.8 容器更新中。

发行说明可在我们的 docker 存储库中找到:


Windows Docker

.NET 6 添加了对 Windows 进程隔离容器的支持。如果您在 Azure Kubernetes 服务 (AKS) 中使用Windows 容器,则您依赖于进程隔离的容器。进程隔离容器可以被认为与 Linux 容器非常相似。Linux 容器使用cgroups,Windows 进程隔离容器使用Job Objects。Windows 还提供 Hyper-V 容器,它通过更大的虚拟化提供更大的隔离。Hyper-V 容器在 .NET 6 中没有变化。

此更改的主要价值是现在将报告 Windows 进程隔离容器的正确值。如果在 64 核机器上创建 2 核容器,将返回. 在以前的版本中,此属性将报告机器上的处理器总数,与 Docker CLI、Kubernetes 或其他容器编排器/运行时指定的限制无关。该值被 .NET 的各个部分用于缩放目的,包括 .NET 垃圾收集器(尽管它依赖于相关的较低级别的 API)。社区库也依赖此 API 进行扩展。Environment.ProcessorCountEnvironment.ProcessorCount2

我们最近在 AKS 上使用大量 Pod 生产的 Windows 容器上与客户一起验证了这项新功能。他们能够用50%的内存运行成功(相比,其典型的配置),先前导致了水平OutOfMemoryException和StackOverflowException例外。他们没有花时间找到最小内存配置,但我们猜测它明显低于他们典型内存配置的 50%。由于这一变化,他们将转向更便宜的 Azure 配置,从而节省资金。这是一个不错的、轻松的胜利,只需升级即可。

优化缩放

我们从用户那里听到一些应用程序在报告正确值时无法实现最佳缩放。如果这听起来与您刚刚阅读的 Windows Containers 内容相反,那么它有点像。.NET 6 现在提供DOTNET_PROCESSOR_COUNT 环境变量来手动控制 的值。在典型用例中,应用程序可能在 64 核机器上配置有 4 个核,并且在 8 核或 16 核方面具有最佳扩展性。此环境变量可用于启用该缩放。Environment.ProcessorCountEnvironment.ProcessorCount

这个模型可能看起来很奇怪,其中和(通过 Docker CLI)值可能不同。默认情况下,容器运行时面向核心等效项,而不是实际核心。这意味着,当您说需要 4 个内核时,您将获得 4 个内核的等效 CPU 时间,但是您的应用程序可能(理论上)在更多内核上运行,甚至在短时间内在 64 核机器上运行所有 64 个内核。这可能会使您的应用程序在 4 个以上的线程上更好地扩展(继续示例),并且分配更多可能是有益的。这假设线程分配基于 的值。如果您选择设置更高的值,您的应用可能会使用更多内存。对于某些工作负载,这是一个简单的权衡。至少,这是一个您可以测试的新选项。Environment.ProcessorCount--cpusEnvironment.ProcessorCount

Linux 和 Windows 容器均支持此新功能。

Docker 还提供了 CPU 组功能,您的应用程序可以关联到特定的内核。在这种情况下不推荐使用此功能,因为应用程序可以访问的内核数量是具体定义的。我们还看到了将它与 Hyper-V 容器一起使用时的一些问题,它并不是真正适用于这种隔离模式。

Debian 11 “靶心”

我们密切关注Linux 发行版的生命周期和发布计划,并尝试代表您做出最佳选择。Debian 是我们用于默认 Linux 映像的 Linux 发行版。如果您6.0从我们的一个容器存储库中提取标签,您将提取一个 Debian 映像(假设您使用的是 Linux 容器)。对于每个新的 .NET 版本,我们都会考虑是否应该采用新的 Debian 版本。

作为政策问题,我们不会为了我们的便利标签而更改 Debian 版本,例如6.0中期发布。如果我们这样做了,某些应用程序肯定会崩溃。这意味着,在发布之初选择 Debian 版本非常重要。此外,这些图像得到了很多使用,主要是因为它们被“好标签”引用。

Debian 和 .NET 版本自然不是一起计划的。当我们开始 .NET 6 时,我们看到 Debian “bullseye”可能会在 2021 年发布。我们决定从发布之初就押注于 Bullseye。我们开始使用.NET 6 Preview 1发布基于 Bullseye 的容器映像,并决定不再回头。赌注是 .NET 6 版本将在与 Bullseye 版本的竞争中失败。到 8 月 8 日,我们仍然不知道 Bullseye 什么时候发货,在我们自己的版本发布前三个月,即 11 月 8 日。我们不想在预览版 Linux 上发布生产 .NET 6,但我们坚持到了我们会输掉这场比赛的计划。

Debian 11 “bullseye”于 8 月 14 日发布时,我们感到惊喜。我们输了比赛,但赢了赌注。这意味着 .NET 6 用户从第一天起就默认获得最好和最新的 Debian。我们相信 Debian 11 和 .NET 6 将成为许多用户的绝佳组合。对不起,克星,我们撞到了靶心

较新的发行版在其软件包源中包含各种软件包的较新主要版本,并且通常可以更快地获得CVE 修复。这是对较新内核的补充。新的发行版可以更好地为用户服务。

展望未来,不久我们将开始计划对Ubuntu 22.04 的支持。Ubuntu是另一个 Debian 家族发行版,深受 .NET 开发人员的欢迎。我们希望为新的 Ubuntu LTS 版本提供当日支持。

Tianon Gravi 致敬,他为社区维护 Debian 映像并在我们遇到问题时帮助我们。

网络监视器

dotnet monitor是容器的重要诊断工具。它作为 sidecar 容器镜像已经有一段时间了,但处于不受支持的“实验”状态。作为 .NET 6 的一部分,我们将发布一个完全支持生产的基于 .NET 6 的dotnet monitor映像

dotnet monitor已被 Azure 应用服务用作其http://ASP.NETCore Linux 诊断体验的实现细节。这是预期的场景之一,建立在 dotnet monitor 之上以提供更高级别和更高价值的体验。

您现在可以拉取新图像:

docker pull mcr.microsoft.com/dotnet/monitor:6.0

dotnet monitor使从 .NET 进程访问诊断信息(日志、跟踪、进程转储)变得更加容易。在台式机上很容易访问您想要的所有诊断信息,但是,例如,那些熟悉的技术在使用容器的生产中可能不起作用。dotnet monitor提供了一种统一的方法来收集这些诊断工件,无论是在您的台式机上还是在 Kubernetes 集群中运行。收集这些诊断工件有两种不同的机制:

  • 用于临时收集工件的HTTP API。当您已经知道您的应用程序遇到问题并且您有兴趣收集更多信息时,您可以调用这些 API 端点。

  • 基于规则的配置触发器,用于始终在线的工件集合。您可以配置规则以在满足所需条件时收集诊断数据,例如,在持续高 CPU 时收集进程转储。

dotnet monitor为 .NET 应用程序提供了一个通用的诊断 API,可以使用任何工具在任何地方使用。“通用 API”不是 .NET API,而是您可以调用和查询的 Web API。dotnet monitor包括一个 ASP.NET Web 服务器,它直接与 .NET 运行时中的诊断服务器交互并公开数据。的设计dotnet monitor支持生产中的高性能监控和安全使用,以控制对特权信息的访问。dotnet monitor通过非互联网可寻址的unix 域套接字与运行时交互——跨越容器边界。该模型通信模型非常适合此用例。

结构化JSON 日志

JSON格式现在是默认控制台记录f="https://hub.docker.com/_/microsoft-dotnet-aspnet">aspnet.NET 6容器图像。.NET 5 中的默认设置为简单的控制台格式化程序。进行此更改是为了拥有一个默认配置,该配置可与依赖于机器可读格式(如 JSON)的自动化工具配合使用。

图像的输出现在如下所示aspnet:

$ docker run --rm -it -p 8000:80 mcr.microsoft.com/dotnet/samples:aspnetapp
{"EventId":60,"LogLevel":"Warning","Category":"Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository","Message":"Storing keys in a directory u0027/root/.aspnet/DataProtection-Keysu0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.","State":{"Message":"Storing keys in a directory u0027/root/.aspnet/DataProtection-Keysu0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.","path":"/root/.aspnet/DataProtection-Keys","{OriginalFormat}":"Storing keys in a directory u0027{path}u0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed."}}
{"EventId":35,"LogLevel":"Warning","Category":"Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager","Message":"No XML encryptor configured. Key {86cafacf-ab57-434a-b09c-66a929ae4fd7} may be persisted to storage in unencrypted form.","State":{"Message":"No XML encryptor configured. Key {86cafacf-ab57-434a-b09c-66a929ae4fd7} may be persisted to storage in unencrypted form.","KeyId":"86cafacf-ab57-434a-b09c-66a929ae4fd7","{OriginalFormat}":"No XML encryptor configured. Key {KeyId:B} may be persisted to storage in unencrypted form."}}
{"EventId":14,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Now listening on: http://[::]:80","State":{"Message":"Now listening on: http://[::]:80","address":"http://[::]:80","{OriginalFormat}":"Now listening on: {address}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Application started. Press Ctrlu002BC to shut down.","State":{"Message":"Application started. Press Ctrlu002BC to shut down.","{OriginalFormat}":"Application started. Press Ctrlu002BC to shut down."}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Hosting environment: Production","State":{"Message":"Hosting environment: Production","envName":"Production","{OriginalFormat}":"Hosting environment: {envName}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Content root path: /app","State":{"Message":"Content root path: /app","contentRoot":"/app","{OriginalFormat}":"Content root path: {contentRoot}"}}

可以通过设置或取消设置Logging__Console__FormatterName环境变量或通过代码更改来更改记录器格式类型(有关更多详细信息,请参阅控制台日志格式)。

更改后,您将看到如下输出(就像 .NET 5):

$ docker run --rm -it -p 8000:80 -e Logging__Console__FormatterName="" mcr.microsoft.com/dotnet/samples:aspnetapp
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {8d4ddd1d-ccfc-4898-9fe1-3e7403bf23a0} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app

注意:此更改不会影响开发人员计算机上的 .NET SDK,与dotnet run. 此更改特定于aspnet容器映像。

支持 OpenTelemetry 指标

我们一直在为最近的几个 .NET 版本添加对 OpenTelemetry 的支持,作为我们对可观察性的关注的一部分。在 .NET 6 中,我们添加了对OpenTelemetry Metrics API 的支持。通过添加对 OpenTelemetry 的支持,您的应用程序可以与其他OpenTelemetry系统无缝互操作。

System.Diagnostics.MetricsOpenTelemetry Metrics API 规范的 .NET 实现。Metrics API 是专门为处理原始测量而设计的,目的是高效且同时地生成这些测量的连续摘要。

API 包括Meter可用于创建仪器对象的类。这些API暴露四个仪器类:Counter,Histogram,ObservableCounter,并ObservableGauge支持不同的指标方案。此外,API 公开MeterListener该类以允许侦听仪器的记录测量以进行聚合和分组。

OpenTelemetry .NET实现将扩展到使用这些新的API,其中新增的指标可观察方案的支持。

库测量记录示例

    Meter meter = new Meter("io.opentelemetry.contrib.mongodb", "v1.0");
    Counter counter = meter.CreateCounter("Requests");
    counter.Add(1);
    counter.Add(1, KeyValuePair.Create("request", "read"));

听力示例

    MeterListener listener = new MeterListener();
    listener.InstrumentPublished = (instrument, meterListener) =>
    {
        if (instrument.Name == "Requests" && instrument.Meter.Name == "io.opentelemetry.contrib.mongodb")
        {
            meterListener.EnableMeasurementEvents(instrument, null);
        }
    };
    listener.SetMeasurementEventCallback((instrument, measurement, tags, state) =>
    {
        Console.WriteLine($"Instrument: {instrument.Name} has recorded the measurement {measurement}");
    });
    listener.Start();

Windows 窗体

我们继续在 Windows 窗体中进行关键改进。.NET 6 包括更好的控件可访问性、设置应用程序范围默认字体的能力、模板更新等。

辅助功能改进

在此版本中,我们增加了UIA提供商为CheckedListBox,LinkLabel,Panel,ScrollBar,TabControl和TrackBar,使像旁白工具和测试自动化与应用程序的元素进行交互。

默认字体

现在,您可以设置默认字体的应用程序有。Application.SetDefaultFont

void Application.SetDefaultFont(Font font)

最少的应用

以下是带有 .NET 6最小 Windows 窗体应用程序

class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.Run(new Form1());
    }
}

作为 .NET 6 版本的一部分,我们一直在更新大多数模板,使其更加现代和简约,包括 Windows 窗体。我们决定让 Windows 窗体模板更传统一些,部分原因是需要将该属性应用于应用程序入口点。然而,除了立即出现之外,还有更多的玩法。[STAThread]

ApplicationConfiguration.Initialize() 是一个源代码生成的 API,它在后台发出以下调用:

Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetDefaultFont(new Font(...));
Application.SetHighDpiMode(HighDpiMode.SystemAware);

这些调用的参数可通过csproj 或 props 文件中的MSBuild 属性进行配置。

Visual Studio 2022 中的 Windows 窗体设计器也知道这些属性(目前它只读取默认字体),并且可以向您展示您的应用程序,就像它在运行时一样:





模板更新

C# 的 Windows 窗体模板已更新,以支持新的应用程序引导程序、指令、文件范围的命名空间和可为空的引用类型。global using

更多运行时设计器

现在您可以构建通用设计器(例如,报表设计器),因为 .NET 6 具有设计器和设计器相关基础结构的所有缺失部分。有关更多信息,请参阅此博客文章

单文件应用程序

在 .NET 6 中,已为 Windows 和 macOS 启用内存中单文件应用程序。在 .NET 5 中,这种部署类型仅限于 Linux。您现在可以为所有支持的操作系统发布作为单个文件部署和启动的单文件二进制文件。单文件应用程序不再将任何核心运行时程序集提取到临时目录。

这种扩展功能基于称为“超级主机”的构建块。“apphost”是在非单文件情况下启动应用程序的可执行文件,例如或。Apphost 包含用于查找运行时、加载它并使用该运行时启动您的应用程序的代码。Superhost 仍然执行其中一些任务,但使用所有 CoreCLR 本机二进制文件的静态链接副本。静态链接是我们用来启用单个文件体验的方法。myapp.exe./myapp

本机依赖项(如 NuGet 包附带的依赖项)是单文件嵌入的显着例外。默认情况下,它们不包含在单个文件中。例如,WPF 本机依赖项不是超级主机的一部分,导致除了单文件应用程序之外还有其他文件。您可以使用该设置IncludeNativeLibrariesForSelfExtract来嵌入和提取本机依赖项。

静态分析

我们改进了单文件分析器以允许自定义警告。如果您的 API 在单文件发布中不起作用,您现在可以使用该属性对其进行标记,如果启用了分析器,则会出现警告。添加该属性还将使方法中与单个文件相关的所有警告静音,因此您可以使用该警告将警告向上传播到您的公共 API。[RequiresAssemblyFiles]

设置为 时,会自动为exe项目启用单文件分析器,但您也可以通过设置为为任何项目启用它。如果您想支持库作为单个文件应用程序的一部分,这会很有帮助。PublishSingleFiletrueEnableSingleFileAnalysistrue

在 .NET 5 中,我们添加了警告和其他一些在单文件包中表现不同的 API。Assembly.Location

压缩

单文件包现在支持压缩,可以通过将属性设置EnableCompressionInSingleFile为true. 在运行时,文件会根据需要解压缩到内存中。压缩可以为某些场景提供巨大的空间节省。

让我们看一下与NuGet 包资源管理器一起使用的带压缩和不带压缩的单个文件发布。

无压缩:172 MB





压缩后:71.6 MB





压缩可以显着增加应用程序的启动时间,尤其是在 Unix 平台上。Unix 平台具有无法与压缩一起使用的无复制快速启动路径。您应该在启用压缩后测试您的应用,看看额外的启动成本是否可以接受。

单文件调试

单文件应用程序目前只能使用平台调试器(如 WinDBG)进行调试。我们正在考虑在 Visual Studio 2022 的更高版本中添加 Visual Studio 调试。

macOS 上的单文件签名

单文件应用程序现在满足 macOS 上的 Apple 公证和签名要求。在具体的变化涉及到我们在离散文件布局方面构建单个文件的应用程序的方式。

苹果开始执行新规定签署和公证MacOS的卡特琳娜。我们一直在与 Apple 密切合作,以了解要求,并寻找解决方案,使 .NET 等开发平台能够在该环境中良好运行。在最近的几个 .NET 版本中,我们已经对产品进行了更改并记录了用户工作流程,以满足 Apple 的要求。剩下的差距之一是单文件签名,这是在 macOS 上分发 .NET 应用程序的要求,包括在 macOS 商店中。

IL 修整

该团队一直致力于为多个版本进行 IL 修整。.NET 6 代表了这一旅程的重要一步。我们一直在努力使更激进的修剪模式安全且可预测,因此有信心将其设为默认值。以前是选择加入功能,现在是默认功能。TrimMode=link

我们有一个三管齐下的修剪策略:

  • 提高平台的修剪能力。

  • 对平台进行注释以提供更好的警告并使其他人也能这样做。

  • 在此基础上,使默认修剪模式更加激进,以便轻松将应用程序变小。

由于使用未注释反射的应用程序的结果不可靠,因此修剪之前一直处于预览状态。有了修剪警告,体验现在应该是可预测的。没有修剪警告的应用程序应该正确修剪并且在运行时观察到行为没有变化。目前,只有核心 .NET 库已经完全注释用于修剪,但我们希望看到生态系统注释用于修剪并成为修剪兼容

减少应用程序大小

让我们来看看使用crossgen 的这种修剪改进,它是 SDK 工具之一。可以只用一些修剪警告来修剪它,crossgen 团队能够解决这些问题。

首先,让我们将 crossgen 发布为一个独立的应用程序,无需修剪。它是 80 MB(包括 .NET 运行时和所有库)。





然后我们可以尝试(现在是旧版).NET 5 默认修剪模式,copyused. 结果下降到 55 MB。





新的 .NET 6 默认修剪模式link将自包含文件大小进一步降低到 36MB。





我们希望新的link修剪模式能更好地满足修剪的期望:显着的节省和可预测的结果。

默认启用警告

修剪警告告诉您修剪可能会删除运行时使用的代码的地方。这些警告以前在默认情况下被禁用,因为警告非常嘈杂,主要是由于 .NET 平台没有作为第一类场景参与修剪。

我们对 .NET 库的大部分进行了注释,以便它们生成准确的修剪警告。因此,我们认为是时候默认启用修剪警告了。http://ASP.NETCore 和 Windows 桌面运行时库尚未注释。我们计划接下来(.NET 6 之后)注释http://ASP.NET服务组件。我们希望看到社区在 .NET 6 发布后对 NuGet 库进行注释。

您可以通过设置为来禁用警告true。

更多信息:

与本机 AOT 共享

我们也为Native AOT 实验实施了相同的修剪警告,这应该会以大致相同的方式改善 Native AOT 编译体验。

数学

我们显着改进了数学 API。社区一些人已经在享受这些改进

面向性能的 API

面向性能的数学 API 已添加到 System.Math。如果底层硬件支持,它们的实现是硬件加速的。

新 API:

  • SinCos用于同时计算Sin和Cos。

  • ReciprocalEstimate用于计算 的近似值。1 / x

  • ReciprocalSqrtEstimate用于计算 的近似值。1 / Sqrt(x)

新的重载:

  • Clamp、DivRem、Min、 和Max支持nint和nuint。

  • Abs和Sign支持nint。

  • DivRem返回 a 的变体tuple。

性能改进:

大整数性能

从十进制和十六进制字符串解析 BigIntegers已得到改进。我们看到了高达 89% 的改进,如下图所示(越低越好)。





归功于约瑟夫·达席尔瓦

Complex API 现在注释为 readonly

href="https://github.com/dotnet/runtime/pull/51797/">现在System.Numerics.Complexreadonly对各种API 进行了注释,以确保不会为readonly通过in.

感谢hrrrrustic

BitConverter 现在支持浮点到无符号整数比特转换

BitConverter ref="https://github.com/dotnet/runtime/pull/53784">现在支持DoubleToUInt64Bits,HalfToUInt16Bits,SingleToUInt32Bits,UInt16BitsToHalf,UInt32BitsToSingle,和UInt64BitsToDouble。这应该可以在需要时更容易地进行浮点位操作。

感谢米哈尔Petryka

BitOperations 支持附加功能

BitOperations现在支持IsPow2RoundUpToPowerOf2为现有函数href="https://github.com/dotnet/runtime/pull/58733">提供nint/nuint重载。

感谢约翰·凯利霍耀元罗宾·林德纳

Vector, Vector2, Vector3, 和Vector4改进

Vectorref="https://github.com/dotnet/runtime/pull/50832">现在支持nintnuintC# 9 中添加原始类型。例如,此更改应该可以更轻松地使用带有指针或平台相关长度类型的 SIMD 指令。

Vectorf="https://github.com/dotnet/runtime/pull/53527">现在支持一种Sum方法来简化需要计算向量中所有元素的“水平总和”。感谢伊万兹拉塔诺夫

Vector"https://githhttp://ub.com/dotnet/runtime/pull/47150">现在支持通用方法As来简化在具体类型未知的通用上下文中处理向量。感谢霍耀元

ref="https://github.com/dotnet/runtime/pull/50062">重载支持Span已添加到Vector2、Vector3和Vector4以改善需要加载或存储向量类型时的体验。

更好地解析标准数字格式

我们改进了标准数字类型的解析器,特别是和。他们现在将理解精度要求 > 99 位小数,并将提供精确到那么多位数的结果。此外,解析器现在更好地支持方法中的尾随零。.ToString.TryFormatParse

以下示例演示了前后行为。

  • 32.ToString("C100") -> C132

    • .NET 6: $32.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

    • .NET 5:我们在格式化代码中存在人为限制,只能处理 <= 99="">= 100,我们将输入解释为自定义格式。


  • 32.ToString("H99") -> 扔一个 FormatException

    • .NET 6:抛出 FormatException

    • 这是正确的行为,但在此处调用它是为了与下一个示例进行对比。


  • 32.ToString("H100") -> H132

    • .NET 6:抛出 FormatException

    • .NET 5:H是无效的格式说明符。所以,我们应该抛出一个FormatException. 相反,我们将精度 >= 100 解释为自定义格式的错误行为意味着我们返回了错误的值。


  • double.Parse("9007199254740997.0") -> 9007199254740998

    • .NET 6: 9007199254740996.

    • .NET 5:9007199254740997.0不能完全以 IEEE 754 格式表示。使用我们当前的舍入方案,正确的返回值应该是9007199254740996。但是,输入的最后一部分迫使解析器错误地舍入结果并返回。.09007199254740998


系统.文本.Json

System.Text.Json提供了多种高性能的 API 来处理 JSON 文档。在过去的几个版本中,我们添加了新功能,以进一步提高 JSON 处理性能并减轻想要从. 此版本包括在这条道路上的继续,并且是性能的重大进步,特别是在序列化器源生成器方面。NewtonSoft.Json

JsonSerializer 源代码生成

注意:应重新编译使用 .NET 6 RC1 或更早版本的源代码生成的应用程序。

几乎所有 .NET 序列化程序的支柱都是反射。反射对于某些场景来说是一项很棒的功能,但不能作为高性能云原生应用程序(通常(反)序列化和处理大量 JSON 文档)的基础。反射是启动、内存使用和程序集修整的问题

运行时反射的替代方案是编译时源代码生成。在 .NET 6 中,我们将一个"https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/">新的源生成器作为System.Text.Json. JSON 源生成器可与JsonSerializer多种方式结合使用并可进行配置。

它可以提供以下好处:

  • 减少启动时间

  • 提高序列化吞吐量

  • 减少私有内存使用

  • 删除运行时使用和System.ReflectionSystem.Reflection.Emit

  • IL 修整兼容性

默认情况下,JSON 源生成器为给定的可序列化类型发出序列化逻辑。JsonSerializer通过生成Utf8JsonWriter直接使用的源代码,这提供了比使用现有方法更高的性能。简而言之,源代码生成器提供了一种在编译时为您提供不同实现的方法,以便使运行时体验更好。

给定一个简单类型:

namespace Test
{
    internal class JsonMessage
    {
        public string Message { get; set; }
    }
}

源生成器可以配置为为示例JsonMessage类型的实例生成序列化逻辑。请注意,类名JsonContext是任意的。您可以为生成的源使用任何您想要的类名。

using System.Text.Json.Serialization;

namespace Test
{
    [JsonSerializable(typeof(JsonMessage)]
    internal partial class JsonContext : JsonSerializerContext
    {
    }
}

使用此模式的序列化程序调用可能类似于以下示例。此示例提供了可能的最佳性能。

using MemoryStream ms = new();
using Utf8JsonWriter writer = new(ms);

JsonSerializer.Serialize(jsonMessage, JsonContext.Default.JsonMessage);
writer.Flush();

// Writer contains:
// {"Message":"Hello, world!"}

最快和最优化的源代码生成模式——基于Utf8JsonWriter——目前仅可用于序列化。Utf8JsonReader根据您的反馈,未来可能会提供类似的反序列化支持——基于——。

源生成器还发出类型元数据初始化逻辑,这也有利于反序列化。要反序列化JsonMessage使用预生成类型元数据的实例,您可以执行以下操作:

JsonSerializer.Deserialize(json, JsonContext.Default.JsonMessage);

JsonSerializer 支持 IAsyncEnumerable

现在,可以(反)序列化JSON阵列IAsyncEnumerable与。以下示例使用流作为数据的任何异步源的表示。源可以是本地机器上的文件,也可以是数据库查询或 Web 服务 API 调用的结果。System.Text.Json

JsonSerializer.SerializeAsync已更新以识别IAsyncEnumerable值并提供特殊处理。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;

static async IAsyncEnumerable PrintNumbers(int n)
{
    for (int i = 0; i < n; i++) yield return i;
}

using Stream stream = Console.OpenStandardOutput();
var data = new { Data = PrintNumbers(3) };
await JsonSerializer.SerializeAsync(stream, data); // prints {"Data":[0,1,2]}

IAsyncEnumerable值仅支持使用异步序列化方法。尝试使用同步方法进行序列化将导致NotSupportedException抛出异常。

流式反序列化需要一个新的 API 返回. 我们为此目的添加了该方法,您可以在以下示例中看到。IAsyncEnumerableJsonSerializer.DeserializeAsyncEnumerable

using System;
using System.IO;
using System.Text;
using System.Text.Json;

var stream = new MemoryStream(Encoding.UTF8.GetBytes("[0,1,2,3,4]"));
await foreach (int item in JsonSerializer.DeserializeAsyncEnumerable(stream))
{
    Console.WriteLine(item);
}

此示例将按需反序列化元素,并且在使用特别大的数据流时非常有用。它只支持从根级 JSON 数组中读取,尽管将来可能会根据反馈放宽。

现有DeserializeAsync方法名义上支持,但在其非流式方法签名的范围内。它必须将最终结果作为单个值返回,如下例所示。IAsyncEnumerable

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;

var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"{""Data"":[0,1,2,3,4]}"));
var result = await JsonSerializer.DeserializeAsync(stream);
await foreach (int item in result.Data)
{
    Console.WriteLine(item);
}

public class MyPoco
{
    public IAsyncEnumerable Data { get; set; }
}

在此示例中,反序列化器将IAsyncEnumerable在返回反序列化对象之前缓冲内存中的所有内容。这是因为反序列化器需要在返回结果之前消耗整个 JSON 值。

System.Text.Json:可写 DOM 功能

写JSON DOM功能增加了一个新的简单的,高性能的编程模型的。这个新的 API 很有吸引力,因为它避免了需要强类型的序列化契约,并且与现有类型相比,DOM 是可变的。System.Text.JsonJsonDocument

这个新的 API 有以下好处:

  • 在不可能或不希望使用POCO类型的情况下,或者当 JSON 模式不固定且必须检查时,序列化的轻量级替代方案。

  • 允许对大树的子集进行有效修改。例如,可以高效地导航到大型 JSON 树的子部分并从该子部分读取数组或反序列化 POCO。LINQ 也可以与它一起使用。

以下示例演示了新的编程模型。

    // Parse a JSON object
    JsonNode jNode = JsonNode.Parse("{"MyProperty":42}");
    int value = (int)jNode["MyProperty"];
    Debug.Assert(value == 42);
    // or
    value = jNode["MyProperty"].GetValue();
    Debug.Assert(value == 42);

    // Parse a JSON array
    jNode = JsonNode.Parse("[10,11,12]");
    value = (int)jNode[1];
    Debug.Assert(value == 11);
    // or
    value = jNode[1].GetValue();
    Debug.Assert(value == 11);

    // Create a new JsonObject using object initializers and array params
    var jObject = new JsonObject
    {
        ["MyChildObject"] = new JsonObject
        {
            ["MyProperty"] = "Hello",
            ["MyArray"] = new JsonArray(10, 11, 12)
        }
    };

    // Obtain the JSON from the new JsonObject
    string json = jObject.ToJsonString();
    Console.WriteLine(json); // {"MyChildObject":{"MyProperty":"Hello","MyArray":[10,11,12]}}

    // Indexers for property names and array elements are supported and can be chained
    Debug.Assert(jObject["MyChildObject"]["MyArray"][1].GetValue() == 11);

ReferenceHandler.IgnoreCycles

JsonSerializer(System.Text.Json)现在支持在序列化对象图时忽略循环的能力。该选项的行为与 Newtonsoft.Json 相似。一个主要区别是 System.Text.Json 实现用JSON 标记替换引用循环,而不是忽略对象引用。ReferenceHandler.IgnoreCyclesReferenceLoopHandling.Ignorenull

您可以在以下示例中看到 的行为。在这种情况下,属性被序列化,因为它否则会创建一个循环。ReferenceHandler.IgnoreCyclesNextnull

class Node
{
    public string Description { get; set; }
    public object Next { get; set; }
}

void Test()
{
    var node = new Node { Description = "Node 1" };
    node.Next = node;

    var opts = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles };

    string json = JsonSerializer.Serialize(node, opts);
    Console.WriteLine(json); // Prints {"Description":"Node 1","Next":null}
}

源代码构建

使用源代码构建,只需使用几条命令即可在您自己的机器上从源代码构建 .NET SDK。让我解释一下为什么这个项目很重要。

自 .NET Core 1.0 发布之前,源代码构建是一个场景,也是我们与 Red Hat 合作开发的基础设施。几年后,我们非常接近提供它的完全自动化版本。对于 Red Hat Enterprise Linux (RHEL) .NET 用户来说,此功能非常重要。红帽告诉我们,.NET 已经发展成为其生态系统的重要开发者平台。好的!

Linux 发行版黄金标准是使用作为发行版存档一部分的编译器和工具链构建开源代码。这适用于 .NET 运行时(用 C++ 编写),但不适用于任何用 C# 编写的代码。对于 C# 代码,我们使用两遍构建机制来满足发行版要求。这有点复杂,但了解流程很重要。

Red Hat 使用 .NET SDK (#1) 的 Microsoft 二进制构建来构建 .NET SDK 源代码,以生成 SDK (#2) 的纯开源二进制构建。之后,使用 SDK 的新版本 (#2) 再次构建相同的 SDK 源代码,以生成可证明的开源 SDK (#3)。.NET SDK (#3) 的最终二进制版本随后可供 RHEL 用户使用。之后,Red Hat 可以使用相同的 SDK (#3) 来构建新的 .NET 版本,而不再需要使用 Microsoft SDK 来构建月度更新。

这个过程可能令人惊讶和困惑。开源发行版需要由开源工具构建。此模式确保不需要 Microsoft 构建的 SDK,无论是有意还是无意。作为开发者平台,被包含在发行版中比仅使用兼容许可证有更高的标准。源构建项目使 .NET 能够满足这一要求。

源代码构建的交付物是一个源代码压缩包。源 tarball 包含 SDK(对于给定版本)的所有源。从那里,红帽(或其他组织)可以构建自己的 SDK 版本。Red Hat 策略需要使用从源代码构建的工具链来生成二进制 tar 球,这就是他们使用两遍方法的原因。但是源代码构建本身不需要这种两遍方法。

在 Linux 生态系统中,为给定组件提供源代码和二进制包或 tarball 是很常见的。我们已经有可用的二进制 tarball,现在也有源 tarball。这使得 .NET 与标准组件模式相匹配。

.NET 6 的重大改进是源 tarball 现在是我们构建的产品。过去,它需要大量的人工来制作,这也导致将源 tarball 传送到 Red Hat 的延迟很长。双方对此都不满意。

五年多以来,我们一直与红帽在这个项目上密切合作。它取得了成功,在很大程度上要归功于我们有幸与之共事的优秀红帽工程师的努力。其他发行版和组织已经并将从他们的努力中受益。

附带说明一下,源代码构建是朝着可重现构建迈出的一大步,我们也坚信这一点。.NET SDK 和 C# 编译器具有重要的可重现构建功能。

库 API

除了已经涵盖的 API 之外,还添加了以下 API。

WebSocket 压缩

压缩对于通过网络传输的任何数据都很重要。WebSockets 现在启用压缩。我们使用了WebSockets的扩展实现,RFC 7692。它允许使用该算法压缩 WebSockets 消息有效负载。此功能是用户对 GitHub 上 Networking 的最高要求之一。permessage-deflateDEFLATE

与加密一起使用的压缩可能会导致攻击,例如CRIMEBREACH。这意味着不能在单个压缩上下文中将秘密与用户生成的数据一起发送,否则可以提取该秘密。为了让用户注意这些影响并帮助他们权衡风险,我们将其中一个关键 API 命名为DangerousDeflateOptions。我们还添加了对特定消息关闭压缩的功能,因此如果用户想要发送机密,他们可以在不压缩的情况下安全地发送。

禁用压缩时 WebSocket内存占用减少了约 27%。

从客户端启用压缩很容易,如下例所示。但是,请记住,服务器可以协商设置,例如请求较小的窗口或完全拒绝压缩。

var cws = new ClientWebSocket();
cws.Options.DangerousDeflateOptions = new WebSocketDeflateOptions()
{
    ClientMaxWindowBits = 10,
    ServerMaxWindowBits = 10
};

还添加了对 ASP.NET Core 的 WebSocket 压缩支持

感谢伊万兹拉塔诺夫

袜子代理支持

SOCKS是一种代理服务器实现,可以处理任何 TCP 或 UDP 流量,使其成为一个非常通用的系统。这是一个长期存在的社区请求,已添加到 .NET 6 中

此更改增加了对 Socks4、Socks4a 和 Socks5 的支持。例如,它允许通过 SSH 测试外部连接或连接到 Tor 网络

该WebProxy班现在接受socks的方案,你可以在下面的例子中看到。

var handler = new HttpClientHandler
{
    Proxy = new WebProxy("socks5://127.0.0.1", 9050)
};
var httpClient = new HttpClient(handler);

感谢霍耀元

Microsoft.Extensions.Hosting — ConfigureHostOptions API

我们在 IHostBuilder 上添加了一个新的 ConfigureHostOptions API 以简化应用程序设置(例如,配置关闭超时):

using HostBuilder host = new()
    .ConfigureHostOptions(o =>
    {
        o.ShutdownTimeout = TimeSpan.FromMinutes(10);
    })
    .Build();

host.Run();

在 .NET 5 中,配置主机选项有点复杂:

using HostBuilder host = new()
    .ConfigureServices(services =>
    {
        services.Configure(o =>
        {
            o.ShutdownTimeout = TimeSpan.FromMinutes(10);
        });
    })
    .Build();

host.Run();

Microsoft.Extensions.DependencyInjection — CreateAsyncScope API

CreateAsyncScopeAPI是为了处理处置IAsyncDisposable服务。以前,您可能已经注意到,对IAsyncDisposable服务提供者的处置可能会引发InvalidOperationException异常。

下面的示例演示了新模式,CreateAsyncScope用于启用using语句的安全使用。

await using (var scope = provider.CreateAsyncScope())
{
    var foo = scope.ServiceProvider.GetRequiredService();
}

下面的例子演示了现有的问题案例:

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

await using var provider = new ServiceCollection()
        .AddScoped()
        .BuildServiceProvider();

// This using can throw InvalidOperationException
using (var scope = provider.CreateScope())
{
    var foo = scope.ServiceProvider.GetRequiredService();
}

class Foo : IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}

以下模式是之前建议的避免异常的解决方法。不再需要它。

var scope = provider.CreateScope();
var foo = scope.ServiceProvider.GetRequiredService();
await ((IAsyncDisposable)scope).DisposeAsync();

感谢马丁Björkström

Microsoft.Extensions.Logging — 编译时源代码生成器

.NET 6href="https://github.com/dotnet/runtime/issues/52549">引入了LoggerMessageAttribute类型. 此属性是命名空间的一部分,使用时,它会源生成高性能日志 API。源代码生成日志支持旨在为现代 .NET 应用程序提供高度可用和高性能的日志解决方案。自动生成的源代码依赖于接口和功能。Microsoft.Extensions.LoggingILoggerLoggerMessage.Define

当LoggerMessageAttribute用于partial日志记录方法时触发源生成器。当被触发时,它要么能够自动生成partial它正在装饰的方法的实现,要么生成带有正确使用提示的编译时诊断。编译时日志记录解决方案在运行时通常比现有日志记录方法快得多。它通过最大限度地消除装箱、临时分配和副本来实现这一点。

与直接手动使用API 相比,有以下好处:LoggerMessage.Define

  • 更短更简单的语法:声明性属性使用而不是编码样板。

  • 引导开发者体验:生成器给出警告,帮助开发者做正确的事。

  • 支持任意数量的日志参数。最多支持六个。LoggerMessage.Define

  • 支持动态日志级别。这是不可能的。LoggerMessage.Define

要使用LoggerMessageAttribute,消费类和方法需要是partial。代码生成器在编译时触发并生成该partial方法的实现。

public static partial class Log
{
    [LoggerMessage(EventId = 0, Level = LogLevel.Critical, Message = "Could not open socket to `{hostName}`")]
    public static partial void CouldNotOpenSocket(ILogger logger, string hostName);
}

在前面的示例中,日志记录方法是,static并且在属性定义中指定了日志级别。在静态上下文中使用属性时,ILogger需要实例作为参数。您也可以选择在非静态上下文中使用该属性。有关更多示例和使用场景,请访问编译时日志源生成器文档。

System.Linq — 可枚举支持Index和Range参数

该方法现在接受可枚举项末尾的索引,如下例所示。Enumerable.ElementAt

Enumerable.Range(1, 10).ElementAt(^2); // returns 9

添加了一个接受参数的重载。它简化了对可枚举序列的切片:Enumerable.TakeRange

  • source.Take(..3) 代替 source.Take(3)

  • source.Take(3..) 代替 source.Skip(3)

  • source.Take(2..7) 代替 source.Take(7).Skip(2)

  • source.Take(^3..) 代替 source.TakeLast(3)

  • source.Take(..^3) 代替 source.SkipLast(3)

  • source.Take(^7..^3)而不是.source.TakeLast(7).SkipLast(3)

感谢@dixin

System.Linq — TryGetNonEnumeratedCount

该TryGetNonEnumeratedCount方法尝试在不强制枚举的情况下获取源可枚举的计数。这种方法在枚举之前预分配缓冲区很有用的情况下很有用,如下例所示。

List buffer = source.TryGetNonEnumeratedCount(out int count) ? new List(capacity: count) : new List();
foreach (T item in source)
{
    buffer.Add(item);
}

TryGetNonEnumeratedCount检查实现ICollection/或利用Linq 使用的一些内部优化的源。ICollection

System.Linq — DistinctBy/ UnionBy/ IntersectBy/ExceptBy

新的变体已添加到集合操作中,允许使用键选择器函数指定相等性,如下面的示例所示。

Enumerable.Range(1, 20).DistinctBy(x => x % 3); // {1, 2, 3}

var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) };
first.UnionBy(second, person => person.Age); // { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40), ("Drew", 33) }

System.Linq — MaxBy/MinBy

MaxBy和MinBy方法允许使用键选择器查找最大或最小元素,如下例所示。

var people = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
people.MaxBy(person => person.Age); // ("Ashley", 40)

System.Linq — Chunk

Chunk 可用于将可枚举的源分块为固定大小的切片,如下例所示。

IEnumerable chunks = Enumerable.Range(0, 10).Chunk(size: 3); // { {0,1,2}, {3,4,5}, {6,7,8}, {9} }

归功于罗伯特·安德森

System.Linq的- FirstOrDefault/ LastOrDefault/SingleOrDefault过载采取默认参数

现有FirstOrDefault/ LastOrDefault/SingleOrDefault方法返回如果源枚举是空的。添加了新的重载,接受在这种情况下要返回的默认参数,如下面的示例所示。default(T)

Enumerable.Empty().SingleOrDefault(-1); // returns -1

感谢@ Foxtrek64

System.Linq —Zip接受三个枚举的重载

邮编方法现在支持组合三个枚举接口,你可以在下面的例子中看到。

var xs = Enumerable.Range(1, 10);
var ys = xs.Select(x => x.ToString());
var zs = xs.Select(x => x % 2 == 0);

foreach ((int x, string y, bool z) in Enumerable.Zip(xs,ys,zs))
{
}

感谢霍耀元

优先队列

PriorityQueue(System.Collections.Generic) 是一个新集合,可以添加具有值和优先级的新项目。在出队时,PriorityQueue 返回具有最低优先级值的元素。您可以将这个新集合视为类似于但每个入队元素都有一个影响出队行为的优先级值。Queue

以下示例演示了.PriorityQueue

// creates a priority queue of strings with integer priorities
var pq = new PriorityQueue();

// enqueue elements with associated priorities
pq.Enqueue("A", 3);
pq.Enqueue("B", 1);
pq.Enqueue("C", 2);
pq.Enqueue("D", 3);

pq.Dequeue(); // returns "B"
pq.Dequeue(); // returns "C"
pq.Dequeue(); // either "A" or "D", stability is not guaranteed.

感谢Patryk Golebiowski

更快地将结构体作为字典值处理

CollectionsMarshal.GetValueRef是一个新的不安全API,它可以更快地更新字典中的结构值。新 API 旨在用于高性能场景,而不是用于一般用途。它返回refstruct 值,然后可以使用典型技术就地更新。

以下示例演示了如何使用新 API:

ref MyStruct value = CollectionsMarshal.GetValueRef(dictionary, key);
// Returns Unsafe.NullRef() if it doesn't exist; check using Unsafe.IsNullRef(ref value)
if (!Unsafe.IsNullRef(ref value))
{
    // Mutate in-place
    value.MyInt++;
}

在此更改之前,更新struct字典值对于高性能场景可能会很昂贵,需要字典查找和struct. 然后在更改 之后struct,它将再次分配给字典键,从而导致另一次查找和复制操作。此改进将密钥散列减少到 1(从 2)并删除所有结构复制操作。

归功于本·亚当斯

新的DateOnly和TimeOnly结构

添加了仅限日期和时间的结构,具有以下特征:

  • 每个代表 a 的一半DateTime,或者只是日期部分,或者只是时间部分。

  • DateOnly是生日、周年纪念日和工作日的理想选择。它符合 SQL Server 的date类型。

  • TimeOnly是定期会议、闹钟和每周工作时间的理想选择。它符合 SQL Server 的time类型。

  • 补充现有的日期/时间类型 ( DateTime, DateTimeOffset, TimeSpan, TimeZoneInfo)。

  • 在System命名空间中,在 CoreLib 中提供,就像现有的相关类型一样。

性能改进 DateTime.UtcNow

这种改进有以下好处:

  • 修复了在 Windows 上获取系统时间的2.5 倍性能回归

  • 利用 5 分钟的 Windows 闰秒数据滑动缓存,而不是在每次调用时获取。

支持所有平台上的 Windows 和 IANA 时区

这种改进有以下好处:

改进的时区显示名称

Unix 上的时区显示名称已得到改进

  • 从 返回的列表中的显示名称中消除歧义。TimeZoneInfo.GetSystemTimeZones

  • 利用 ICU / CLDR 全球化数据。

  • 仅适用于 Unix。Windows 仍然使用注册表数据。这可能会在以后更改。

还进行了以下额外改进:

改进了对 Windows ACL 的支持

System.Threading.AccessControl现在包括对与 Windows 访问控制列表 (ACL) 交互的改进支持。为、和的OpenExisting和TryOpenExisting方法添加了新的重载。这些具有“安全权限”实例的重载允许打开使用特殊 Windows 安全属性创建的线程同步对象的现有实例。EventWaitHandleMutexSemaphore

此更新与 .NET Framework 中可用的 API 相匹配,并且具有相同的行为。

以下示例演示如何使用这些新 API。

对于Mutex:

var rights = MutexRights.FullControl;
string mutexName = "MyMutexName";

var security = new MutexSecurity();
SecurityIdentifier identity = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
MutexAccessRule accessRule = new MutexAccessRule(identity, rights, AccessControlType.Allow);
security.AddAccessRule(accessRule);

// createdMutex, openedMutex1 and openedMutex2 point to the same mutex
Mutex createdMutex = MutexAcl.Create(initiallyOwned: true, mutexName, out bool createdNew, security);
Mutex openedMutex1 = MutexAcl.OpenExisting(mutexName, rights);
MutexAcl.TryOpenExisting(mutexName, rights, out Mutex openedMutex2);

为了 Semaphore

var rights = SemaphoreRights.FullControl;
string semaphoreName = "MySemaphoreName";

var security = new SemaphoreSecurity();
SecurityIdentifier identity = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
SemaphoreAccessRule accessRule = new SemaphoreAccessRule(identity, rights, AccessControlType.Allow);
security.AddAccessRule(accessRule);

// createdSemaphore, openedSemaphore1 and openedSemaphore2 point to the same semaphore
Semaphore createdSemaphore = SemaphoreAcl.Create(initialCount: 1,  maximumCount: 3, semaphoreName, out bool createdNew, security);
Semaphore openedSemaphore1 = SemaphoreAcl.OpenExisting(semaphoreName, rights);
SemaphoreAcl.TryOpenExisting(semaphoreName, rights, out Semaphore openedSemaphore2);

为了 EventWaitHandle

var rights = EventWaitHandleRights.FullControl;
string eventWaitHandleName = "MyEventWaitHandleName";

var security = new EventWaitHandleSecurity();
SecurityIdentifier identity = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
EventWaitHandleAccessRule accessRule = new EventWaitHandleAccessRule(identity, rights, AccessControlType.Allow);
security.AddAccessRule(accessRule);

// createdHandle, openedHandle1 and openedHandle2 point to the same event wait handle
EventWaitHandle createdHandle = EventWaitHandleAcl.Create(initialState: true, EventResetMode.AutoReset, eventWaitHandleName, out bool createdNew, security);
EventWaitHandle openedHandle1 = EventWaitHandleAcl.OpenExisting(eventWaitHandleName, rights);
EventWaitHandleAcl.TryOpenExisting(eventWaitHandleName, rights, out EventWaitHandle openedHandle2);

HMAC 一次性方法

HMAC类现在有静态方法,允许HMACs的一次性计算不分配。这些添加类似于先前版本中添加的用于哈希生成的一次性方法。System.Security.Cryptography

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();
    }
}

它可用于创建高级系统,例如复杂的缓存系统或该类型的自定义版本。例如,MVVM 工具包中的类型将使用它来避免广播消息时的内存分配。ConditionalWeakTableWeakReferenceMessenger

可移植线程池

.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 的未来版本中默认启用 Dynamic PGO,希望与 .NET 7 一起使用。我们强烈建议您在您的应用程序中尝试使用 Dynamic PGO 并向我们提供反馈。

完整的 PGO

要获得动态 PGO 的全部优势,您可以设置两个额外的环境变量:和. 这确保了尽可能多的方法参与分层编译。我们称这种变体为Full PGO。与动态 PGO 相比,完整 PGO 可以提供更大的稳态性能优势,但启动时间会更慢(因为必须在第 0 层执行更多方法)。DOTNET_TC_QuickJitForLoops=1DOTNET_ReadyToRun=0

您不希望将此选项用于短期运行的无服务器应用程序,但对于长期运行的应用程序可能有意义。

在未来的版本中,我们计划精简和简化这些选项,以便您可以更简单地获得完整 PGO 的好处,并适用于更广泛的应用程序。

静态 PGO

我们目前使用静态 PGO来优化 .NET 库程序集,如随 R2R(准备运行)提供的程序集。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和。记录结构是一个新的例子。在 .NET 5 和 .NET 6 中,我们一直在提高结构的性能,部分是通过确保结构可以保存在超快的 CPU 寄存器中,当它们是局部变量、参数或方法的返回值时)。这对于使用向量计算的 API 尤其有益。Span

稳定性能测量

团队中有大量的工程系统工作从未出现在博客上。这适用于您使用的任何硬件或软件产品。JIT 团队开展了一个项目来稳定性能测量,目标是增加我们内部性能实验室自动化自动报告的回归值。这个项目很有趣,因为进行了深入的调查以及实现稳定性所需的产品更改。它还展示了我们衡量保持和提高绩效的规模。





此图像展示了不稳定的性能测量,其中性能在连续运行中在慢速和快速之间波动。x 轴是测试日期,y 轴是测试时间(以纳秒为单位)。在图表的末尾(在提交这些更改之后),您可以看到测量值稳定下来,结果最好。此图像演示了单个测试。还有更多测试在dotnet/runtime #43227中被证明具有类似的行为。

准备运行的代码 / Crossgen 2

Crossgen2 是crossgen 工具的替代品。它旨在满足两个结果:

  • 使跨代开发更高效。

  • 启用一组当前无法通过 crossgen 实现的功能。

这种转换有点类似于本机代码 csc.exe 到托管代码Roslyn 编译器。Crossgen2 是用 C# 编写的,但是,它没有像 Roslyn 那样公开花哨的 API。

我们可能已经/已经为 .NET 6 和 7 计划了六个依赖于 crossgen2 的项目。矢量指令默认值提议是我们想要为 .NET 6 进行的 crossgen2 功能和产品更改的一个很好的例子,但更有可能是 .NET 7。版本气泡是另一个很好的例子。

Crossgen2 支持跨操作系统和架构维度的交叉编译(因此得名“crossgen”)。这意味着您将能够使用单个构建机器为所有目标生成本机代码,至少与准备运行的代码相关。然而,运行和测试该代码是另一回事,为此您需要适当的硬件和操作系统。

第一步是用crossgen2编译平台本身。我们使用 .NET 6 完成了所有架构。因此,我们能够在此版本中淘汰旧的 crossgen。请注意,crossgen2 仅适用于 CoreCLR,不适用于基于 Mono 的应用程序(它们具有一组单独的代码生成工具)。

这个项目——至少在开始时——并不以性能为导向。目标是为托管 RyuJIT(或任何其他)编译器提供更好的架构,以离线方式(不需要或启动运行时)生成代码。

您可能会说“嘿……如果 crossgen2 是用 C# 编写的,那么您不必启动运行时来运行它吗?” 是的,但这不是本文中“离线”的意思。当 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,然后我们将回到对“crossgen2”使用“crossgen”这个术语。

.NET 诊断:EventPipe

EventPipe 是我们的跨平台机制,用于在进程内或进程外输出事件、性能数据和计数器。从 .NET 6 开始,我们已将实现从 C++ 移至 C。通过此更改,Mono 也使用 EventPipe。这意味着 CoreCLR 和 Mono 使用相同的事件基础结构,包括 .NET 诊断 CLI 工具。

这一变化还伴随着 CoreCLR 的小幅缩小:


我们还进行了一些更改,以在负载下提高 EventPipe 吞吐量。在最初的几个预览版中,我们进行了一系列更改,使吞吐量提高了 .NET 5 所能达到的 2.06 倍:





对于此基准测试,越高越好。.NET 6 是橙色线,.NET 5 是蓝色线。

开发工具包

对 .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动词查询更新的工作负载清单、更新本地清单、下载已安装工作负载的新版本,然后删除工作负载的所有旧版本。这类似于(在基于 Debian 的 Linux 发行版上使用)。将工作负载视为 SDK 的私有包管理器是合理的。它是私有的,因为它仅可用于 SDK 组件。我们将来可能会重新考虑这一点。nuget.orgapt update && apt upgrade -y

这些dotnet workload命令在给定 SDK 的上下文中运行。假设您同时安装了 .NET 6 和 .NET 7。工作负载命令将为每个 SDK 提供不同的结果,因为工作负载会有所不同(至少相同工作负载的不同版本)。

请注意,dotnet workload install将工作负载从 NuGet.org 复制到您的 SDK 安装中,因此sudo如果 SDK 安装位置受到保护(意味着在管理员/根位置),则需要提升运行或使用。

内置SDK版本检查

为了更轻松地跟踪新版本的 SDK 和运行时何时可用,我们向 .NET 6 SDK 添加了一个新命令。

dotnet sdk check

它会告诉您是否有更新版本可用于您已安装的任何 .NET SDK、运行时或工作负载。您可以在下图中看到新体验。





dotnet new

您现在可以在http://NuGet.org中搜索带有.dotnet new --search

模板安装的其他改进包括支持开关以支持私有 NuGet 源的授权凭据。--interactive

安装 CLI 模板后,您可以通过和检查更新是否可用。--update-check--update-apply

NuGet 包验证

包验证工具使 NuGet 库开发人员能够验证他们的包是否一致且格式良好。

这包括:

  • 验证跨版本没有重大更改。

  • 验证包对于所有特定于运行时的实现是否具有相同的公共 API 集。

  • 确定任何目标框架或运行时适用性差距。

此工具是 SDK 的一部分。使用它的最简单方法是在项目文件中设置一个新属性。

 true

更多 Roslyn 分析仪

在 .NET 5 中,我们随 .NET SDK 提供了大约 250 个分析器。其中许多已经存在,但作为 NuGet 包在带外发布。我们为 .NET 6 添加了更多分析器

默认情况下,大多数新分析器在信息级别启用。您可以通过启用这些分析仪在警告级别配置的分析模式是这样的:。All

我们发布了我们想要的 .NET 6 分析器集(加上一些额外的东西),然后将其中的大部分都准备好了。社区添加了几个实现,包括这些。

感谢Meik TranelNewell Clark

为平台兼容性分析器启用自定义防护

该CA1416平台兼容性分析仪已经可以识别使用的方法平台警卫OperatingSystem和RuntimeInformation,如和。但是,分析器不识别任何其他保护可能性,例如缓存在字段或属性中的平台检查结果,或者在辅助方法中定义了复杂的平台检查逻辑。OperatingSystem.IsWindowsOperatingSystem.IsWindowsVersionAtLeast

为了允许自定义保护的可能性,我们添加了新属性SupportedOSPlatformGuard并UnsupportedOSPlatformGuard使用相应的平台名称和/或版本注释自定义保护成员。该注释被平台兼容性分析器的流分析逻辑识别和尊重。

用法

    [UnsupportedOSPlatformGuard("browser")] // The platform guard attribute
#if TARGET_BROWSER
    internal bool IsSupported => false;
#else
    internal 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 开发人员。


来源:知乎