1
头图

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

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

请参阅ASP.NET CoreEntity FrameworkWindows Forms.NET MAUIYARPdotnet 监视器帖子,了解各种场景中的新增功能。

.NET 6 亮点

.NET 6 是:

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

支持

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

Red Hat与.NET 团队合作,在Red Hat Enterprise 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 Code C# 扩展支持.NET 6 。

Azure App 服务:

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

统一扩展平台

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

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

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

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

面向 .NET 6

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

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

<TargetFramework>net6.0<TargetFramework>

net6.0 Target Framework Moniker (TFM) 使您可以访问.NET 提供的所有跨平台API。如果您正在编写控制台应用程序、ASP.NET Core 应用程序或可重用的跨平台库,这是最佳选择。

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

  • net6.0-android
  • net6.0-ios
  • net6.0-maccatalyst
  • net6.0-tvos
  • net6.0-windows

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

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

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

性能

自从我们启动.NET Core 项目以来,该团队一直在不断地关注性能。Stephen Toub在记录每个版本的.NET 性能进展方面做得非常出色。欢迎查看.NET 6 中的性能改进的帖子。在这篇文章中,里面包括您想了解的重大性能改进,包括文件IO、接口转换、PGO 和System.Text.Json。

动态 PGO

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

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

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

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

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

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

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

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

方法意思是代码大小
PGO 已禁用1.905 纳秒30乙
启用PGO0.7071 纳秒105乙

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

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

作为上下文,接口调度是 .NET 中最昂贵的调用类型。 非虚拟方法调用是最快的,甚至更快的是可以通过内联消除的调用。 在这种情况下,动态 PGO 为 MoveNext 提供了两个(替代)调用站点。 第一个 - 热的 - 是对 Enumerable+RangeIterator.MoveNext的直接调用,另一个 - 冷的 - 是通过 IEnumerator<int>的虚拟接口调用。 如果大多数时候最热门的人都被叫到,那将是一个巨大的胜利。

这就是魔法。当 JIT 检测此方法的第 0 层代码时,包括检测此接口调度以跟踪每次调用时 \_source的具体类型。 JIT 发现每次调用都在一个名为 Enumerable+RangeIterator的类型上,这是一个私有类,用于在 Enumerable实现内部实现 Enumerable.Range。因此,对于第 1 层,JIT 已发出检查以查看 \_source的类型是否为 Enumerable+RangeIterator:如果不是,则跳转到我们之前强调的执行正常接口调度的冷部分。但如果是 - 基于分析数据,预计绝大多数时间都是这种情况 - 然后它可以继续直接调用非虚拟化的 Enumerable+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倍的分配下降

方法运行意思是比率已分配
写100MBAsync.NET 5.01,308.2 毫秒1.003,809 KB
写100MBAsync.NET 6.0306.8 毫秒0.243 KB

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

async Task AllOrNothingAsync(string path, IReadOnlyList<ReadOnlyMemory<byte>> 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++ 模式)。仅仅能够阅读构成运行时的一些代码是培养以各种形式做出贡献的信心的重要一步。

归功于 Ben Adams

System.Text.Json 源生成器

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

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

序列化POCO是一种非常常见的场景。使用新的源代码生成器,我们观察到序列化速度比我们的基准 快1.6倍

方法意思是标准差比率
串行器243.1 纳秒9.54 纳秒1.00
SrcGenSerializer149.3 纳秒1.91 纳秒0.62

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

请求 / 要求
net5.0243,0003,669,151
网6.0260,9283,939,804
net6.0 + JSON 源码生成364,2245,499,468

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

 C# 10

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

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

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

全局使用指令

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

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

  • global using System;
  • global using static System.Console;
  • global using Env = System.Environment;

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

隐式 usings 是一个MSBuild 概念,它会根据SDK自动添加一组指令。例如,控制台应用程序隐式使用不同于ASP.NET Core。

隐式使用是可选的,并在a 中启用PropertyGroup

  • <ImplicitUsings\&gt;enable\&lt;/ImplicitUsings>

隐式使用对于现有项目是可选的,但默认包含在新C# 项目中。有关详细信息,请参阅隐式使用

文件范围的命名空间

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

新语法是单个的一行:

namespaceMyNamespace;

classMyClass{...}// Not indented

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

namespaceMyNamespace
{
classMyClass{...}// Everything is indented
}

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

记录结构

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

public record structPerson
{
publicstringFirstName{get; init;}
publicstringLastName{get; init;}
}

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

public record structPerson(stringFirstName,stringLastName);

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

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

publicreadonly record structPerson(stringFirstName,stringLastName);

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

var updatedPerson = person with{FirstName=&quot;Mary&quot;};

F# 6

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

让 F# 更快、更互操作

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

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

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

让 F# 更易学、更统一

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

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

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

改进 F# 工具

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

热重载

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

Hot Reload 可通过dotnet watch CLI 工具和 Visual Studio 2022 使用。您可以将 Hot Reload 与多种应用类型一起使用,例如 ASP.NET Core、Blazor、.NET MAUI、控制台、Windows 窗体 (WinForms)、WPF、WinUI 3、Azure 函数等。

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

此图像显示了一个使用dotnet watch. 我对.cs文件和.cshtml文件进行了编辑(如日志中所述),两者都应用于代码并在不到半秒的时间内非常快速地反映在浏览器中。

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

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

Hot Reload 还与现有的 Edit and Continue 功能(在断点处停止时)以及用于实时编辑应用程序 UI 的 XAML Hot Reload 协同工作。目前支持 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 时,您最有可能开始使用 OpenSSL 3。大多数发行版还没有这样做。例如,如果您在 Red Hat 8 或 Ubuntu 20.04 上安装 .NET 6,您将不会(在撰写本文时)开始使用 OpenSSL 3。

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

感谢 Kevin Jones对 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 运行,例如-adotnet run -a x64. 相同的论点适用于其他 CLI 动词。有关更多信息,请参阅 适用于macOS 和Windows Arm64 的.NET 6 RC2 更新

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

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 也不需要它们。

视窗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 上本机运行。我将向您展示ASP.NET Core 的样子。

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 的默认设置,以及使用-a参数在目标Arm64 和x64 之间切换是多么容易。完全相同的体验适用于Windows Arm64。

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

Arm64 上的 Docker

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

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

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

自己试试吧: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 容器

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

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

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

优化缩放

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

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

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

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

Debian 11 "bullseye"

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

作为一项政策,我们不会为了方便标签而更改Debian 版本,例如6.0, mid-release。如果我们这样做了,某些应用程序肯定会崩溃。这意味着,在发布开始时选择Debian 版本非常重要。此外,这些图像得到了很多使用,主要是因为它们是"好标签"的引用。

Debian 和.NET 版本自然不会一起计划。当我们开始.NET 6 时,我们看到Debian "bullseye" 可能会在2021 年发布。我们决定从发布开始就押注于Bullseye我们开始使用.NET 6 Preview 1发布基于靶心的容器映像,并决定不再回头。赌注是.NET 6 版本会输掉与靶心版本的竞争。到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

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

dotnet monitor已被Azure App Service 用作其ASP.NET Core 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通过非Internet 可寻址的unix domain socket与运行时交互——跨越容器边界。该模型通信模型非常适合此用例。

结构化 JSON 日志

JSON 格式化程序现在是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 公开了四个工具类:CounterHistogramObservableCounter和,ObservableGauge以支持不同的度量方案。此外,API 公开MeterListener该类以允许收听仪器记录的测量值,以用于聚合和分组目的。

OpenTelemetry .NET 实现将被扩展以使用这些新的API,这些API 添加了对Metrics 可观察性场景的支持。

图书馆测量记录示例

     Meter meter = new Meter("io.opentelemetry.contrib.mongodb", "v1.0");
    Counter<int> counter = meter.CreateCounter<int>("Requests");
    counter.Add(1);
    counter.Add(1, KeyValuePair.Create<string, object>("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<int>((instrument, measurement, tags, state) =>
    {
        Console.WriteLine($"Instrument: {instrument.Name} has recorded the measurement {measurement}");
    });
    listener.Start();

Windows Forms

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

可访问性改进

在此版本中,我们添加了用于CheckedListBoxLinkLabelPanelScrollBarTabControlTrackBarUIA 提供程序,它们使讲述人等工具和测试自动化能够与应用程序的元素进行交互。

默认字体

您现在可以使用.Application.SetDefaultFont

voidApplication.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(newFont(...));
Application.SetHighDpiMode(HighDpiMode.SystemAware);

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

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

模板更新

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

更多运行时 designers

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

单文件应用

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

这种扩展功能基于称为"超级主机"的构建块。"apphost" 是在非单文件情况下启动应用程序的可执行文件,例如myapp.exe./myapp. Apphost 包含用于查找运行时、加载它并使用该运行时启动您的应用程序的代码。Superhost 仍然执行其中一些任务,但使用所有CoreCLR 本机二进制文件的静态链接副本。静态链接是我们用来实现单一文件体验的方法。本机依赖项(如NuGet 包附带的)是单文件嵌入的显着例外。默认情况下,它们不包含在单个文件中。例如,WPF 本机依赖项不是超级主机的一部分,因此会在单文件应用程序之外产生其他文件。您可以使用该设置IncludeNativeLibrariesForSelfExtract嵌入和提取本机依赖项。

静态分析

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

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

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

压缩

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

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

无压缩: 172 MB

压缩: 71.6 MB

压缩会显着增加应用程序的启动时间,尤其是在Unix 平台上。Unix 平台有一个不能用于压缩的无拷贝快速启动路径。您应该在启用压缩后测试您的应用程序,看看额外的启动成本是否可以接受。

单文件调试

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

macOS 上的单文件签名

单文件应用程序现在满足macOS 上的Apple 公证和签名要求。具体更改与我们根据离散文件布局构建单文件应用程序的方式有关。

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

IL 修整

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

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

  • 提高平台的修剪能力。
  • 对平台进行注释以提供更好的警告并使其他人也能这样做。
  • 在此基础上,让默认的修剪模式更具侵略性,以便让应用程序变小。

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

减小应用程序大小

让我们使用SDK 工具之一的crossgen来看看这个修剪改进。它可以通过几个修剪警告进行修剪,crossgen 团队能够解决。

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

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

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

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

默认启用警告

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

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

您可以通过设置<SuppressTrimAnalysisWarnings>true来禁用警告。

更多信息:

与本机 AOT 共享

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

数学

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

面向性能的 API

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

新 API:

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

新的重载:

  • Clamp, DivRem,MinMax支持nintnuint
  • AbsSign支持nint
  • DivRem 变体返回tuple

性能改进:

大整数性能

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

感谢约瑟夫·达席尔瓦

Complex API 现在注释为 readonly

现在对各种API 进行了注释,System.Numerics.Complexreadonly以确保不会对readonly值或传递的值进行复制in。

归功于hrrrrustic 。

BitConverter 现在支持浮点到无符号整数位广播

BitConverter 现在支持DoubleToUInt64Bits, HalfToUInt16Bits, SingleToUInt32Bits, UInt16BitsToHalf, UInt32BitsToSingle, 和UInt64BitsToDouble. 这应该使得在需要时更容易进行浮点位操作。

归功于Michal Petryka 。

BitOperations 支持附加功能

BitOperations现在支持IsPow2,RoundUpToPowerOf2提供nint/nuint重载现有函数

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

Vector<T>, Vector2, Vector3 和 Vector4 改进

Vector<T>现在支持C# 9 中添加的原始类型nint和nuint原始类型。例如,此更改应该可以更简单地使用带有指针或平台相关长度类型的SIMD 指令。

Vector<T>现在支持一种Sum方法来简化计算向量中所有元素的“水平和”的需要。归功于伊万兹拉塔诺夫

Vector<T>现在支持一种通用方法As<TFrom, TTo>来简化在具体类型未知的通用上下文中处理向量。感谢霍耀源

重载支持Span<T>已添加到Vector2Vector3Vector4以改善需要加载或存储矢量类型时的体验。

更好地解析标准数字格式

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

以下示例演示了之前和之后的行为。

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

    • .NET 6:<details>
      </details>$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

System.Text.Json

System.Text.Json提供多种高性能API 用于处理JSON 文档。在过去的几个版本中,我们添加了新功能,以进一步提高JSON 处理性能并减轻对希望从NewtonSoft.Json迁移的人的阻碍。 此版本包括在该路径上的继续,并且在性能方面向前迈出了一大步,特别是在序列化程序源生成器方面。

JsonSerializer 源生成

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

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

运行时反射的替代方法是编译时源代码生成。在.NET 6 中,我们包含一个新的源代码生成器作为 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

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

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

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

static async IAsyncEnumerable<int> 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 来返回IAsyncEnumerable<T>。我们为此添加了JsonSerializer.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<int>(stream))
{
    Console.WriteLine(item);
}

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

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

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<MyPoco>(stream);
await foreach (int item in result.Data)
{
    Console.WriteLine(item);
}

public class MyPoco
{
    public IAsyncEnumerable<int> Data { get; set; }
}

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

System.Text.Json:可写 DOM 功能

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

这个新的 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<int>();
    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<int>();
    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<int>() == 11);

ReferenceHandler.IgnoreCycles

JsonSerializer(System.Text.Json)现在支持在序列化对象图时忽略循环的能力。该ReferenceHandler.IgnoreCycles选项具有与Newtonsoft.Json ReferenceLoopHandling.Ignore类似的行为。一个关键区别是System.Text.Json 实现用null JSON 标记替换引用循环,而不是忽略对象引用。

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

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 用户来说,这个功能很重要。Red Hat 告诉我们,.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 的扩展permessage-deflate实现,RFC 7692。它允许使用该DEFLATE算法压缩WebSockets 消息负载。此功能是GitHub 上Networking 的主要用户请求之一。

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

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

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

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

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

归功于伊万兹拉塔诺夫

Socks 代理支持

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

归功于Huo yaoyuan。

Microsoft.Extensions.Hosting — 配置主机选项 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<HostOptions>(o =>
        {
            o.ShutdownTimeout = TimeSpan.FromMinutes(10);
        });
    })
    .Build();

host.Run();

Microsoft.Extensions.DependencyInjection — CreateAsyncScope API

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

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

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

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

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

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

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

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

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

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

感谢Martin Björkström 。

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

.NET 6 引入了LoggerMessageAttribute类型。 此属性是Microsoft.Extensions.Logging命名空间的一部分,使用时,它会源生成高性能日志记录API。源生成日志支持旨在为现代.NET 应用程序提供高度可用和高性能的日志解决方案。自动生成的源代码依赖于ILogger接口和LoggerMessage.Define功能。

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

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

  • 更短更简单的语法:声明性属性使用而不是编码样板。
  • 引导式开发人员体验:生成器发出警告以帮助开发人员做正确的事情。
  • 支持任意数量的日志记录参数。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.Take接受Range参数的重载。它简化了对可枚举序列的切片:

  • 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<T> buffer = source.TryGetNonEnumeratedCount(out int count) ? new List<T>(capacity: count) : new List<T>();
foreach (T item in source)
{
    buffer.Add(item);
}

TryGetNonEnumeratedCount检查实现ICollection/ ICollection<T>;或利用Linq 采用的一些内部优化的源

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

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

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<int[]> chunks = Enumerable.Range(0, 10).Chunk(size: 3); // { {0,1,2}, {3,4,5}, {6,7,8}, {9} }

归功于罗伯特安德森

System.Linq—— // FirstOrDefault 采用默认参数的重载 LastOrDefaultSingleOrDefault

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

Enumerable.Empty\&lt;int\&gt;().SingleOrDefault(-1); // returns -1

感谢@ Foxtrek64 。

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

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))
{
}

归功于Huo yaoyuan。

优先队列

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

以下示例演示了.PriorityQueue<string, int>

// creates a priority queue of strings with integer priorities
var pq = new PriorityQueue<string, int>();

// 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 旨在用于高性能场景,而不是用于一般用途。它返回ref结构值,然后可以使用典型技术对其进行更新。

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

ref MyStruct value = CollectionsMarshal.GetValueRef(dictionary, key);
// Returns Unsafe.NullRef<TValue>() 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 倍性能回归。
  • 利用Windows 闰秒数据的5 分钟滑动缓存,而不是在每次调用时获取。

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

这种改进具有以下好处:

  • 使用时的隐式转换(https://github.com/dotnet/run...TimeZoneInfo.FindSystemTimeZoneById
  • TimeZoneInfo通过: TryConvertIanaIdToWindowsIdTryConvertWindowsIdToIanaIdHasIanaId(https://github.com/dotnet/run...)上的新API 进行显式转换
  • 改进了使用不同时区类型的系统之间的跨平台支持和互操作。
  • 删除需要使用TimeZoneConverter OSS 库。该功能现在是内置的。

改进的时区显示名称

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

  • 消除由.返回的列表中的显示名称的歧义。TimeZoneInfo.GetSystemTimeZones
  • 利用ICU / CLDR 全球化数据。
  • 仅适用于Unix。Windows 仍然使用注册表数据。这可能会在以后更改。

还进行了以下附加改进:

  • UTC 时区的显示名称和标准名称被硬编码为英语,现在使用与其余时区数据相同的语言(CurrentUICulture在Unix 上,Windows 上的操作系统默认语言)。
  • 由于大小限制,Wasm 中的时区显示名称改为使用非本地化IANA ID。
  • TimeZoneInfo.AdjustmentRule嵌套类将其BaseUtcOffsetDelta内部属性公开,并获得一个新的构造函数,该构造函数baseUtcOffsetDelta作为参数。(https://github.com/dotnet/run...
  • TimeZoneInfo.AdjustmentRule还获得了在Unix 上加载时区的各种修复(https://github.com/dotnet/run...), (https://github.com/dotnet/run...)

改进了对 Windows ACL 的支持

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

此更新与.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 一次性方法

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

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 -\&gt; 640K)。这是一个惊人的改进,无需更改代码。

我们的目标是在未来的.NET 版本中默认启用动态PGO,希望在.NET 7 中启用。我们强烈建议您在应用程序中尝试动态PGO 并向我们提供反馈。

完整的 PGO

要充分利用Dynamic PGO,您可以设置两个额外的环境变量:DOTNET\_TC\_QuickJitForLoops=1DOTNET\_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 库中的性能原语。最近的例子ValueTaskValueTupleSpan<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.so7037856 – 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 &amp;&amp; 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\&lt;,\&gt;.Keys.ContainsContainsKey
纽厄尔·克拉克dotnet/运行时#45552使用代替String.EqualsString.Compare
梅克特雷尔dotnet/运行时#47180使用代替String.Contains(char)String.Contains(String)

感谢Meik TranelNewell Clark

为 Platform Compatibility Analyzer 启用自定义防护

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

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

用法

    [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 开发人员。


MicrosoftReactor
109 声望19 粉丝

微软 Reactor 是微软为构建开发者社区而提供的一个社区空间,以“予力多元化社区建设,帮助每一个开发者成就不凡”为使命,旨在通过不定期举办的技术讲座、开发者交流会面及技术沙龙和专题活动,帮助开发者和初创企...