作者:Jon Douglas - Principal Program Manager, NuGet
翻译:Alan Wang
排版:Alan Wang
我们很高兴与大家分享在 .NET 8 预览版 4 中的所有新功能和改进!这次发布是继预览版 3之后的更新。您将在这些月度发布中看到更多功能逐渐亮相。.NET 6 和 7 的用户将希望密切关注此版本,因为我们专注于使升级过程变得非常简单。
您可以为 Linux、macOS 和 Windows 下载 .NET 8 预览版 4。
- 安装程序和二进制文件
- 容器镜像
- 发行说明
- 已知问题
- GitHub 问题跟踪器
Microsoft Build 2023 即将到来! .NET 团队将举办一系列活动,从技术深度挖掘到与团队进行问答。在 Microsoft Build 2023 中加入 .NET 团队!
查看在预览版 4 发布中 ASP.NET Core 和 EF Core 中的新功能。了解最新的即将上线的 .NET 8 的新功能。在整个发布过程中都将保持更新。
最后,.NET 8 已经与 17.7 预览版 1 进行了测试。我们建议您使用预览渠道构建,如果您想尝试在 Visual Studio 系列产品中使用 .NET 8。Visual Studio for Mac 对 .NET 8 的支持尚未推出。如果您坚持使用稳定渠道,请查看 Visual Studio 17.6 发布中的最新功能和改进。
现在,让我们来看一些 .NET 8 的新功能。
MSBuild:全新的现代化终端构建输出
- https://github.com/dotnet/msbuild/issues/8370
我们经常收到用户反馈,指出默认的 MSBuild 输出(在内部称为控制台日志记录器)难以解析。它相当静态,通常是一大堆文字,而且在构建过程中会实时显示错误,而不是作为构建项目的一部分逻辑显示。我们认为这些都是很好的反馈意见,并很高兴推出我们对 MSBuild 输出日志更新、更现代化的第一个迭代版本。我们称之为 Terminal Logger,它有以下几个主要目标:
- 将错误与所属项目进行逻辑分组
- 以用户认为的构建方式展示项目/构建(尤其是多目标项目)
- 更好地区分项目构建的 TargetFrameworks
- 继续提供关于项目输出的概览信息
- 在整个构建过程中提供关于构建当前正在执行的操作的信息。
这是它的样子:
可以使用 /tl 以及以下选项之一来启用新的输出:
auto - 默认设置,会在启用新日志记录器之前检查终端是否能够使用新功能,并且没有使用重定向的标准输出;
on - 重写上述环境检测,并强制使用新的日志记录器;
off - 重写上述环境检测,并强制使用之前的控制台日志记录器。
一旦启用,新的日志记录器将显示恢复阶段,然后是构建阶段。在每个阶段,当前正在构建的项目位于终端底部,每个正在构建的项目都会告诉您当前正在构建的 MSBuild Target 以及在该目标上花费的时间。我们希望这些信息让用户更清楚地了解构建过程,并为他们提供一个起点,以便在他们想要了解更多关于构建的信息时进行搜索!当项目全部构建完成后,将为每个构建编写一个“构建完成”段,用来捕获:
- 已构建项目的名称
- 目标框架(如果是多目标的!)
- 构建的状态
- 构建的主要输出(超链接以便快速访问)
- 最后是由该项目的构建生成的所有诊断信息
这个示例没有诊断信息 - 让我们看另一个在同一项目中引入了拼写错误的构建:
在这里,您可以清楚地看到项目和拼写错误。
我们认为这种布局符合 .NET 的现代感,并利用现代终端的功能向用户提供更多关于他们构建的信息。我们希望您会尝试使用它,并为我们提供反馈,告诉我们它对您有什么作用以及您希望在这里看到的其他信息。我们希望将此日志记录器作为 MSBuild 新一批用户体验改进的基础 - 包括未来的进度报告和结构化错误等方面!在使用过程中,请通过这个调查或通过 MSBuild 代码库的讨论区向我们提供反馈。我们期待听到大家的声音!
这项工作是由 Eduardo Villalpando 启发并开始的,他是我们冬季实习生,深入研究了问题并真正帮助我们开辟了道路。没有他的帮助和对问题的热情,这一切都不可能实现!
SDK:简化的输出路径更新
- https://github.com/dotnet/designs/pull/281
- https://github.com/dotnet/sdk/pull/31955
在预览版 3中,我们发布了 .NET SDK 项目的全新简化的输出路径布局,并请您提供关于使用新布局的反馈和经验。感谢您如此做!您的反馈引发了许多讨论,根据我们从尝试更改的每个人那里听到的反馈,我们对此功能进行了以下更新:
- 新布局的默认路径从 .artifacts 更改为 artifacts
- 我们将取消从 Project 文件而不是 Directory.Build.props 使用该功能的能力
- 我们通过将所需属性作为 dotnet new 的 buildprops 模板的选项,使您更容易开始使用此功能
我想深入了解导致我们进行这些更改的思考过程。你们几乎一致支持将文件夹名称中的 . 删除,主要是为了在 Unix 系统上的可见性,在那里 . 通常表示“隐藏”的文件或文件夹。所以我们知道我们想做出这个改变。然而,我们最初不想使用 artifacts 作为根路径有两个主要原因:
- .gitignore 支持
- .NET SDK 文件 globbing 障碍
我们不希望人们只为尝试这个功能就突然必须处理他们的 .gitignore 文件更改,但经过一些研究,我们发现一些面向未来、有进取心的贡献者(谢谢 @sayedihashimi!)已经确保将 artifacts 包含在所有常见的 .gitignore 文件模板中。这意味着我们不必担心用户意外地保存二进制文件。
我们还不想在 .NET SDK 用于在项目中查找要构建的源文件的默认 glob 模式中意外包含 artifacts 输出。如果我们将根路径从 .artifacts 更改为 artifacts 并让用户从项目文件级别使用新功能,那么我们还必须更改所有使 SDK 项目文件如此简洁的默认包含内容。这看起来非常容易出错,而且坦率地说,在用户体验上是一个失败的陷阱。因此,我们已经加强了使用该功能的要求 - 您现在必须通过 Directory.Build.props 文件选择该功能。这具有使特性更稳定的副作用。在此更改之前,推断的根文件夹位置会在创建 Directory.Build.props 文件时发生变化。现在,由于必须存在 Directory.Build.props 文件,Artifacts 路径的位置应保持稳定。
要尝试该功能的新版本,我们已经简化了生成正确的 Directory.Build.props 文件:只需运行 dotnet new buildprops --use-artifacts,我们将为您生成所需的一切。生成的 Directory.Build.props 文件如下所示:
<Project>
<!-- See https://aka.ms/dotnet/msbuild/customize for more details on customizing your build -->
<PropertyGroup>
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
</PropertyGroup>
</Project>
请尝试这些更改,并继续通过我们对此功能的调查告诉我们您的看法。
模板引擎:来自 Nuget.org 的包的安全体验
在 .NET 8 中,我们将把 NuGet.org 的几个安全相关功能集成到模板引擎中,尤其是在 dotnet new 体验中。
改进
- 阻止从 http:\ 供稿中下载包,但允许使用 –force 进行重写
NuGet 团队有一个逐步过渡到默认安全立场的记录计划。您可以在 HTTPS Everywhere 博客文章中了解更多关于他们的计划以及所涉及的时间线。为了支持这个目标,当使用非 HTTPS 源时,我们将默认开始报错。这可以通过 –force 在 .NET 8 的时间范围内重写,但当前计划是在 .NET 9 时间范围内删除此标志,以符合 HTTPS Everywhere 时间线。
- 如果模板软件包在安装/更新/过期检查时存在任何漏洞,请通知客户,并请求 –force 来安装易受攻击的版本
- 向搜索和卸载命令添加数据,显示模板是否从在 NuGet.org 中具有保留前缀的软件包中安装
- 添加关于模板包所有者的信息。所有权经过 nuget 门户验证,可以被视为值得信赖的特征
NuGet:在 Linux 上的签名包验证
从 .NET 8 预览版 4 SDK 开始,NuGet 将默认在 Linux 上验证签名包。验证在 Windows 上保持启用,并在 macOS 上禁用。
对于大多数 Linux 用户,验证应该透明地进行。然而,位于 /etc/pki/ca-trust/extracted/pem/objsign-ca-bundle.pem 的现有根证书束的用户可能会看到伴随 NU3042 的信任失败。
用户可以通过将环境变量 DOTNET_NUGET_SIGNATURE_VERIFICATION 设置为 false 来选择不进行验证。请提供您的反馈以帮助 NuGet 团队改进 Linux 上的体验!
更多信息请参阅 https://github.com/dotnet/core/issues/7688。
NuGet:审核软件包依赖关系以查找安全漏洞
- https://github.com/NuGet/Home/issues/8087
- https://github.com/NuGet/Home/pull/12310
当您选择使用 NuGet 安全审计时,dotnet restore 将生成一个安全漏洞报告,其中包括受影响的包名称、漏洞严重性以及指向咨询更多详细信息的链接。
启用安全审计
当您希望接收安全审计报告时,您可以通过在 .csproj 或作为项目的一部分进行评估的 MSBuild 文件中设置以下 MSBuild 属性来选择加入此体验:
<NuGetAudit>true</NuGetAudit>
当您希望接收安全审计报告时,您可以通过在 .csproj 或作为项目的一部分进行评估的 MSBuild 文件中设置以下 MSBuild 属性来选择加入此体验:
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
dotnet add package
当您尝试添加具有已知漏洞的包时,dotnet restore 将隐式运行并通过警告让您知道。
dotnet restore
当您通过 dotnet restore 恢复软件包时,您将看到每个受影响的软件包和建议的警告。
Warning codes
Warning Code | Severity |
---|---|
NU1901 | low |
NU1902 | moderate |
NU1903 | high |
NU1904 | critical |
设置安全审核级别
您可以将 MSBuild 属性设置为期望的审核失败级别。可选值有 low、moderate、high 和 critical。例如,如果您只想查看 moderate、high 和 critical 建议,则可以设置以下内容:
<NuGetAuditLevel>moderate</NuGetAuditLevel>
库:UTF8 改进
在 .NET 8 Preview 4 中,我们引入了新的 IUtf8SpanFormattable 接口,就像它的表亲 ISpanFormattable 一样,可以在类型上实现,以便将该类型的类似字符串表示形式写入目标 span。ISpanFormattable 面向 UTF16 和 Span,而 IUtf8SpanFormattable 则面向 UTF8 和 Span。除此之外,所有原始类型(以及其他类型)都已实现这个接口,无论是针对 string、Span 还是 Span,都使用相同的共享逻辑(多亏了静态抽象接口),这意味着它完全支持所有格式(包括在 .NET 8 Preview 4 中新增的 “B” 二进制说明符)和所有区域设置。这意味着您现在可以直接从 Byte、Complex 、Char 、DateOnly、DateTime、DateTimeOffset 、Decimal、Double、Guid、Half、IPAddress、IPNetwork、Int16、Int32、Int64、Int128、IntPtr、NFloat、SByte、Single、Rune、TimeOnly、TimeSpan、UInt16、UInt32、UInt64、UInt128、UIntPtr ,以及 Version格式化为 UTF8。
此外,新的 Utf8.TryWrite 方法现在为现有的基于 UTF16 的 MemoryExtensions.TryWrite 方法提供了基于 UTF8 的对应方法。这些方法依赖于 .NET 6 和 C# 10 中引入的插值字符串处理器支持,使您可以使用插值字符串语法将复杂表达式直接格式化为 UTF8 字节的跨度,例如:
static bool FormatHexVersion(short major, short minor, short build, short revision, Span<byte> utf8Bytes, out int bytesWritten) =>
Utf8.TryWrite(utf8Bytes, CultureInfo.InvariantCulture, $"{major:X4}.{minor:X4}.{build:X4}.{revision:X4}", out bytesWritten);
实现可识别格式值上的 IUtf8SpanFormattable,并使用它们的实现直接将 UTF8 表示写入目标跨度。
实现还利用了新方法 Encoding.TryGetBytes,该方法及其对应方法 Encoding.TryGetChars 支持将编码/解码输出到目标跨度,只要跨度足够长以容纳生成的状态,并在长度不足时返回 false 而不是抛出异常。
我们预计在后续的 .NET 8 预览版本中出现更多 UTF8 的改进,包括但不限于此功能的性能改进。
引入时间抽象
TimeProvider 抽象类的引入增加了时间抽象,使得在测试场景中可以模拟时间。这个功能也被其他依赖时间推移的特性所支持,例如 Task.Delay 和 Task.Async。这意味着即使是 Task 操作也可以轻松地使用时间抽象进行模拟。抽象支持获取本地和 UTC 时间,获取性能测量的时间戳以及创建计时器等基本时间操作。
public abstract class TimeProvider
{
public static TimeProvider System { get; }
protected TimeProvider()
public virtual DateTimeOffset GetUtcNow()
public DateTimeOffset GetLocalNow()
public virtual TimeZoneInfo LocalTimeZone { get; }
public virtual long TimestampFrequency { get; }
public virtual long GetTimestamp()
public TimeSpan GetElapsedTime(long startingTimestamp)
public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
public virtual ITimer CreateTimer(TimerCallback callback, object? state,TimeSpan dueTime, TimeSpan period)
}
public interface ITimer : IDisposable, IAsyncDisposable
{
bool Change(TimeSpan dueTime, TimeSpan period);
}
public partial class CancellationTokenSource : IDisposable
{
public CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider)
}
public sealed partial class PeriodicTimer : IDisposable
{
public PeriodicTimer(TimeSpan period, TimeProvider timeProvider)
}
public partial class Task : IAsyncResult, IDisposable
{
public static Task Delay(System.TimeSpan delay, System.TimeProvider timeProvider)
public static Task Delay(System.TimeSpan delay, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken)
public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider)
public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken)
}
public partial class Task<TResult> : Task
{
public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider)
public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken)
}
此外,我们在 .NET 8.0 中提供了抽象,并创建了一个名为Microsoft.Bcl.TimeProvider 的网络标准2.0库。这使得可以在 .NET Framework 以及 .NET 早期版本的支持版本上使用抽象。
namespace System.Threading.Tasks
{
public static class TimeProviderTaskExtensions
{
public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken = default)
public static Task<TResult> WaitAsync<TResult>(this Task<TResult> task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default)
public static Tasks.Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default)
public static CancellationTokenSource CreateCancellationTokenSource(this TimeProvider timeProvider, TimeSpan delay)
}
}
用例
// Get System time
DateTimeOffset utcNow= TimeProvider.System.GetUtcNow();
DateTimeOffset localNow = TimeProvider.System.GetLocalNow();
// Create a time provider that work with a time zone different than the local time zone
private class ZonedTimeProvider : TimeProvider
{
private TimeZoneInfo _zoneInfo;
public ZonedTimeProvider(TimeZoneInfo zoneInfo) : base()
{
_zoneInfo = zoneInfo ?? TimeZoneInfo.Local;
}
public override TimeZoneInfo LocalTimeZone { get => _zoneInfo; }
public static TimeProvider FromLocalTimeZone(TimeZoneInfo zoneInfo) => new ZonedTimeProvider(zoneInfo);
}
// Create a time using a time provider
ITimer timer = timeProvider.CreateTimer(callBack, state, delay, Timeout.InfiniteTimeSpan);
// Measure a period using the system time provider
long providerTimestamp1 = TimeProvider.System.GetTimestamp();
long providerTimestamp2 = TimeProvider.System.GetTimestamp();
var period = GetElapsedTime(providerTimestamp1, providerTimestamp2);
https://github.com/dotnet/runtime/issues/36617
System.Runtime.Intrinsics.Vector512 和 AVX-512
自从我们在 .NET Framework 中首次引入支持以来,SIMD 支持已经成为 .NET 多年来的基本功能。在 .NET Core 3.0 中,我们扩展了对 x86/x64 平台特定硬件内在 API 的支持。我们在 .NET 5 中再次扩展了这一点,支持 Arm64,并在 .NET 7 中引入跨平台硬件内在功能。.NET 8 也不例外,继续通过引入 System.Runtime.Intrinsics.Vector512 及其在具有 AVX-512 支持的 x86/x64 硬件上的加速来进一步提高我们的支持。
AVX-512 本身带来了几个关键功能,其中 Preview 4 添加了对前三个功能的支持。最后一个仍然是我们希望在未来的进展中分享更多详细信息的工作:
- 支持 512 位向量操作
- 支持额外的 16 个 SIMD 寄存器
- 支持 128 位、256 位和 512 位向量可用的额外指令
- 支持掩码向量操作
如果您的硬件支持这个功能,那么 Vector512.IsHardwareAccelerated 将报告 true。我们还在 System.Runtime.Intrinsics.X86 命名空间下暴露了几个平台特定的类,包括 Avx512F(基础)、Avx512BW(字节和字)、Avx512CD(冲突检测)、Avx512DQ(双字和四字)以及 Avx512Vbmi(向量字节操作指令)。这些类的形式和布局与其他 ISA 相似,它们暴露了一个 IsSupported 属性和一个仅用于 64 位进程的指令嵌套类。此外,我们现在在每个类中都有一个嵌套类,该类为相应的指令集公开了 Avx512VL(向量长度)扩展。
由于上面列出的第二和第三个关键功能,即使您在代码中没有明确使用 Vector512 或特定的 Avx512F 指令,您仍然可能从这个功能中受益。这是因为当使用 Vector128 或 Vector256 时,JIT 能够隐式地利用这些功能,包括 BCL 中使用硬件内在函数的所有地方,如 Span 和 ReadOnlySpan 中公开的大多数操作,许多原始类型的数学 API 等等。
这个功能已经有许多 Pull Requests 投入使用,它是许多人的努力成果,尤其是 https://github.com/anthonycanino, https://github.com/DeepakRajendrakumaran 和 https://github.com/jkrishnavs。
Native AOT 改进
我们已经更新了默认 console 模板并添加了对 AOT 的开箱即用支持。现在可以调用 dotnet new console --aot 来创建为 AOT 编译配置的项目。通过 –aot 添加的项目配置有三个效果:
- 使用 dotnet publish 或 Visual Studio 发布项目时,将生成具有本地 AOT 的本地自包含可执行文件。
- 启用针对修剪、AOT 和单文件的基于 Roslyn 的兼容性分析器,这些分析器将在您选择的编辑器中标记项目中可能存在问题的部分(如果有的话)。
- 在没有 AOT 编译的情况下进行调试时,启用 AOT 的调试时模拟功能,以便获得与 AOT 类似的体验。这样可以确保例如在未针对 AOT 注释的 NuGet 包中使用 Reflection.Emit(因此被兼容性分析器忽略)在您首次尝试使用 AOT 发布项目时不会让您大吃一惊。
我们还继续改进使用 Native AOT 的基本性能,如运行时吞吐量、内存使用和磁盘上的大小。在 Preview 4 中,我们添加了一种表达优化偏好的方式,例如速度或大小。默认设置尝试在这两者之间取得平衡,但现在我们也引入了一种方法来指定如何权衡。
例如,在 x64 Windows 上针对 dotnet new console --aot 的结果进行大小优化,在 Preview 4 中可以实现以下节省:
Default | Optimize for Size | |
---|---|---|
Hello World | 1.20 MB | 1.07 MB |
以上是一个完全自包含的应用程序的大小,该应用程序包括运行时(包括 GC)和所有必要的类库。
在 Preview 4中,我们观察到,对速度进行优化可以使真实工作负载的吞吐量提高2-3%。
Linux 发行版版本支持
我们之前宣布将更新 .NET 8 支持的 Linux 发行版版本。这些更改包含在 Preview 4 中,特别是 .NET 8 针对的 glibc 版本。
.NET 8 是针对 Ubuntu 16.04 构建的,适用于所有体系结构。这对于定义 .NET 8 的最低 glibc 版本非常重要。.NET 8 无法在包含较旧的 glibc 发行版版本(例如 Ubuntu 14.04 或 Red Hat Enterprise Linux 7)上启动。
我们也在更新 .NET 8 Linux 以使用 clang 16。我们预计这一变更将包含在 Preview 5 中。我们不会为这个变更另外发布公告。
没有其他重大变化。我们将继续在 Arm32、Arm64 和 x64 架构上支持 Linux 上的 .NET。
System.Text.Json:填充只读成员
从 .NET 8 Preview 4 开始,System.Text.Json 引入了反序列化到只读属性或字段的能力。
我们还引入了一个选项,允许开发者为所有可填充的属性启用它 - 例如自定义转换器可能与此功能不兼容:
JsonSerializerOptions options = new()
{
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate
};
对于现有希望使用此功能但担心兼容性的应用程序,可以通过在要填充的属性类型上放置 [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] 属性来逐个启用该功能。
例如,要对一个特定类的所有属性启用填充:
using System.Text.Json;
using System.Text.Json.Serialization;
JsonSerializerOptions options = new()
{
WriteIndented = true,
// Instead of granular control we could also enable this globally like this:
// PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate
};
CustomerInfo customer = JsonSerializer.Deserialize<CustomerInfo>("""{"Person":{"Name":"John"},"Company":{"Name":"John and Son"}}""", options)!;
Console.WriteLine(JsonSerializer.Serialize(customer, options));
class PersonInfo
{
// there is nothing here to be populated since string cannot be re-used
public required string Name { get; set; }
public string? Title { get; set; }
}
class CompanyInfo
{
public required string Name { get; set; }
public string? Address { get; set; }
public string? PhoneNumber { get; set; }
public string? Email { get; set; }
}
// notes:
// - attribute does not apply to the `CustomerInfo` class itself: i.e. properties of type `CustomerInfo` wouldn't be auto-populated
// - automatic rules like these can be implemented with contract customization
// - attribute do apply to `Person` and `Company` properties
// - attribute can also be placed on individual properties
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
class CustomerInfo
{
private const string NA = "N/A";
// note how neither of these have setters
public PersonInfo Person { get; } = new PersonInfo() { Name = "Anonymous", Title = "Software Developer" };
public CompanyInfo Company { get; } = new CompanyInfo() { Name = NA, Address = NA, PhoneNumber = NA, Email = NA };
}
上述输出与通过全局选项实现的输出相同:
{
"Person": {
"Name": "John",
"Title": "Software Developer"
},
"Company": {
"Name": "John and Son",
"Address": "N/A",
"PhoneNumber": "N/A",
"Email": "N/A"
}
}
之前,为作比较,我们会看到我们的输入,但由于没有可设置的属性 Person 或 Company 来反序列化,我们会完全忽略输入,并且输出只显示默认值:
{
"Person": {
"Name": "Anonymous",
"Title": "Software Developer"
},
"Company": {
"Name": "N/A",
"Address": "N/A",
"PhoneNumber": "N/A",
"Email": "N/A"
}
}
填充只读成员的其他注意事项
- 更多信息请参阅设计的原始 issue:https://github.com/dotnet/runtime/issues/78556
- 结构体也可以进行填充,但填充过程是先创建一个副本,然后将其设置回属性,因此这些属性也需要设置器
- 集合的填充以累加方式进行 - 将现有集合及其所有内容视为原始对象,因此保留了所有现有元素 - 此行为可以通过契约定制和/或反序列化回调进行更改
System.Text.Json 改进
JsonSerializer.IsReflectionEnabledByDefault
- https://github.com/dotnet/runtime/pull/83844
JsonSerializer 类公开了许多可接受可选参数 JsonSerializerOptions 的序列化和反序列化方法。如果未指定,则这些方法默认使用基于反射的序列化器。在经过裁剪/Native AOT 应用程序的上下文中,这个默认值可能会对应用程序大小产生影响:即使用户小心地传递一个源生成的值,它仍然会导致修剪器根据反射组件进行操作。
System.Text.Json 现在附带了用于控制 JsonSerializer 方法默认行为的功能开关 System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault。在发布时将该开关设置为 false 现在可以避免意外地固定反射组件。应该注意的是,禁用该开关后,这段代码
JsonSerializer.Serialize(new { Value = 42 });
会因 NotSupportedException 异常而失败。需要显式传递已配置的 JsonSerializerOptions 才能使方法正常工作。
此外,功能开关的值反映在 JsonSerializer.IsReflectionEnabledByDefault 属性中,该属性被视为链接时常量。基于 System.Text.Json 构建的库作者可以依赖该属性来配置其默认值,同时不会意外地固定反射组件。
{
if (JsonSerializer.IsReflectionEnabledByDefault)
{
// This branch has a dependency on DefaultJsonTypeInfo
// but will get trimmed away by the linker if the feature switch is disabled.
return new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
PropertyNamingPolicy = JsonNamingPolicy.KebabCase,
}
}
return new() { PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower } ;
}
JsonSerializerOptions.TypeInfoResolverChain
- https://github.com/dotnet/runtime/issues/83095
在 .NET 7 中发布时,契约定制功能通过 JsonTypeInfoResolver.Combine 方法添加了对链接源生成器的支持。
var options = new JsonSerializerOptions
{
TypeInfoResolver = JsonTypeInfoResolver.Combine(ContextA.Default, ContextB.Default, ContextC.Default);
};
根据我们收到的反馈,这种方法存在一些可用性问题:
- 它需要在一个调用站点指定所有链式组件 —— 事实上,解析器无法在链之前或之后添加。
- 因为链式实现是抽象在 IJsonTypeInfoResolver 实现后面的,用户无法检查链或从中移除组件。
JsonSerializerOptions 类现在包括一个与 TypeInfoResolver 相辅相成的属性:TypeInfoResolverChain:
namespace System.Text.Json;
public partial class JsonSerializerOptions
{
public IJsonTypeInfoResolver? TypeInfoResolver { get; set; }
public IList<IJsonTypeInfoResolver> TypeInfoResolverChain { get; }
}
现在可以按如下方式操作原始示例中定义的选项实例:
options.TypeInfoResolverChain.Count; // 3
options.TypeInfoResolverChain.RemoveAt(0);
options.TypeInfoResolverChain.Count; // 2
需要注意的是,TypeInfoResolver 和 TypeInfoResolverChain 属性始终保持同步,因此对一个属性的更改将强制对另一个属性进行更新。
弃用的 JsonSerializerOptions.AddContext
- https://github.com/dotnet/runtime/issues/83280
JsonSerializerOptions.AddContext 已被 TypeInfoResolver 和 TypeInfoResolverChain 属性取代,因此它现在被标记为废弃。
无法形容的类型支持
- https://github.com/dotnet/runtime/issues/82457
编译器生成或“无法形容”的类型在弱类型的源代码 gen scenaria 中很难得到支持。在 .NET 7 中,以下应用
object value = Test();
JsonSerializer.Serialize(value, MyContext.Default.Options);
async IAsyncEnumerable<int> Test()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(1000);
yield return i;
}
}
[JsonSerializable(typeof(IAsyncEnumerable<int>))]
internal partial class MyContext : JsonSerializerContext {}
将运行失败并报错
Metadata for type 'Program+<<<Main>$>g__Test|0_5>d' was not provided by TypeInfoResolver of type 'MyContext'
这是因为编译器生成的类型 Program+<$>g_Test|0_5>d 不能由源生成器显式指定。
从 Preview 4 开始,System.Text.Json 将执行运行时最近祖先解析,以确定用于序列化值的最合适的超类型(在本例中,是 IAsyncEnumerable<int>)。
JsonSerializerOptions.TryGetTypeInfo
- https://github.com/dotnet/runtime/pull/84411
Preview 4 现在包括 GetTypeInfo 方法的 Try- 变体,如果找不到指定类型的元数据,该方法将返回 false。
Codegen
连续寄存器分配
在此预览版本中,我们在我们的寄存器分配器中引入了一种名为“连续寄存器”分配的新功能。在深入了解它包含什么以及为什么有必要之前,让我们首先回顾一下什么是寄存器分配以及它在 RyuJIT 中是如何工作的。
RyuJIT 中使用的寄存器分配算法基于“线性扫描”方法。它扫描程序以识别所有变量的生命周期,这些变量在文献中被称为“间隔”,并且在每次使用时为每个变量分配一个寄存器。为了确定在给定点分配最佳寄存器,该算法需要识别在该点活动的变量,并且不与其他变量重叠。然后,它从一组可用的空闲寄存器中选择一个寄存器,在分配点使用启发式方法来确定最佳寄存器集。如果没有寄存器可用,因为它们都分配给了间隔,那么算法会识别可以在该位置“溢出”并分配的最佳寄存器。溢出涉及将寄存器的值存储在堆栈上,稍后在需要时检索它,这是寄存器分配器试图最小化的昂贵操作。
Arm64 有两个指令,TBL 和 TBX,它们用于表向量查找。这些指令将“元组”作为其操作数之一,该操作数可以包含2、3或4个实体。在 PR#80297 中,我们在 AdvSimd 命名空间下为这些指令添加了两组 API,分别为 VectorTableLookup 和 VectorTableLookupExtension。然而,这些指令要求元组中的所有实体都存在于连续的寄存器中。为了更好地理解这个要求,让我们看一个例子。
public static Vector128<byte> Test(float f)
{
var a = Produce1();
var b = Produce2();
var c = a + b;
var d = c + a;
var e = d + b;
d = AdvSimd.Arm64.VectorTableLookup((d, e, e, b), c);
}
这是为该方法生成的代码。
movz x0, #0xD1FFAB1E // code for helloworld:Produce1():System.Runtime.Intrinsics.Vector128`1[ubyte]
movk x0, #0xD1FFAB1E LSL #16
movk x0, #0xD1FFAB1E LSL #32
ldr x0, [x0]
blr x0
str q0, [fp, #0x20] // [V01 loc0]
movz x0, #0xD1FFAB1E // code for helloworld:Produce2():System.Runtime.Intrinsics.Vector128`1[ubyte]
movk x0, #0xD1FFAB1E LSL #16
movk x0, #0xD1FFAB1E LSL #32
ldr x0, [x0]
blr x0
ldr q16, [fp, #0x20] // [V01 loc0]
add v17.16b, v16.16b, v0.16b
str q17, [fp, #0x10] // [V03 loc2]
add v16.16b, v17.16b, v16.16b
add v18.16b, v16.16b, v0.16b
mov v17.16b, v18.16b
mov v19.16b, v0.16b
ldr q20, [fp, #0x10] // [V03 loc2]
tbl v16.16b, {v16.16b, v17.16b, v18.16b, v19.16b}, v20.16b
add v0.16b, v0.16b, v16.16b
在给定的示例中,VectorTableLookup() 接受一个由4个矢量 d, e, e 和 b 组成的元组,它们通过连续的寄存器 v16 到 v19 传递。尽管第2个和第3个值都是相同的变量 e,但它们仍然在不同的寄存器 v17 和 v18 中传递。这带来了寻找不仅是多个空闲(或忙碌)寄存器(2、3 或 4 个)用于 tbl 和 tbx 指令,而且是连续寄存器的复杂性。为了容纳这个新要求,我们的算法必须在各个阶段进行更新,例如在为元组的第一个实体分配寄存器时提前检查连续寄存器是否空闲,确保已分配的寄存器是连续的,如果变量已经分配了寄存器并且它们不连续,以及在可用时添加压力测试场景以处理交替寄存器。在 PR#85189 中,@MihaZupan 在 ProbabilisticMap 的 IndexOf 方法中使用 VectorTableLookup,得到了30%的提升。
优化 ThreadStatic 字段访问
访问标有 ThreadStatic 的字段必须通过辅助调用,这些调用会在访问字段数据之前访问当前线程和模块的线程局部存储(TLS)。在 PR#82973 中,我们将所有这些代码内联,因此可以在不进入辅助函数的情况下检索字段的值。这将字段访问性能提高了10倍。
Arm64
我们继续提高 Arm64 的代码质量,我们的朋友 @SwapnilGaikwad 和 @a74nh 在 Arm 为该版本做出了一些很好的贡献。
- 在 PR#84350 中,将“str wzr”对优化并替换为了“str xzr”。
在 PR#84135 中,针对 SIMD 寄存器启用了 ldp/stp 的窥孔优化。
在 PR#83458 中,在可能的情况下,用成本较低的 mov 指令替换了负载。
在 PR#79283 中,将 if 语句中的条件与比较链组合。
在 PR#82031 中,在可能的情况下开始使用 cinc 而不是 csel。
在 PR#84667 中,将’neg’和’cmp’组合为’cmn’
在 PR#84605 中,将 cmp 和 shift 操作组合成一个 cmp 操作
在 PR#83694 中,添加 IsVNNeverNegative(改进了所有架构,但对 ARM64 影响较大)
直到现在,如果其中一个值来自局部变量,那么将不会执行 load/store pair 窥孔优化。PR#84399 修复了这个限制,并广泛启用了窥孔优化。
在 PR#85258 中,将 Arm64 上的 >>> 运算符优化为 ShiftRightLogical 内部指令。
社区 PR(非常感谢 JIT 社区贡献者们!)
- @SingleAccretion 在 Preview 4 中贡献了20个 PR。其中许多工作集中在内部清理和简化那些在 JIT 上工作的每个人都需要理解的概念上。例如,JIT 内部 IR 中的许多节点类型完全被移除,以支持更规则或更简单的表示。
- @Ruihan-Yin 在 LinearScan:: buildPhysRegRecords 上添加了关于 zmm 寄存器的宏,PR#83862。
- 请参考 CodeGen Arm64 部分以了解 @a74nh 和 @SwapnilGaikwad 的贡献。
代码向量化
JIT/NativeAOT 现在可以使用 SIMD(包括 x64 上的 AVX-512 指令!)展开并自动向量化各种内存操作,如比较、复制和归零,如果它可以在编译时确定它们的大小:
- PR#83255 通过 SIMD 使 stackalloc 归零速度提高了2-3倍
- PR#83638、PR#83740 和 PR#84530 为各种类似“复制缓冲区”的操作启用自动向量化。
- PR#83945 对所有原始类型进行了相同的比较,包括 SequenceEqual 和 StartsWith。以下代码片段是 JIT 现在可以自动向量化的一个很好的示例模式:
bool CopyFirst50Items(ReadOnlySpan<int> src, Span<int> dst) =>
src.Slice(0, 50).TryCopyTo(dst);
```csharp
```asm
; Method CopyFirst50Items
push rbp
vzeroupper
mov rbp, rsp
cmp edx, 50 ;; src.Length >= 50 ?
jb SHORT G_M1291_IG05
xor eax, eax
cmp r8d, 50 ;; dst.Length >= 50 ?
jb SHORT G_M1291_IG04
vmovdqu zmm0, zmmword ptr [rsi]
vmovdqu zmm1, zmmword ptr [rsi+40H]
vmovdqu zmm2, zmmword ptr [rsi+80H]
vmovdqu xmm3, xmmword ptr [rsi+B8H]
vmovdqu zmmword ptr [rcx], zmm0
vmovdqu zmmword ptr [rcx+40H], zmm1
vmovdqu zmmword ptr [rcx+80H], zmm2
vmovdqu xmmword ptr [rcx+B8H], xmm3
mov eax, 1
G_M1291_IG04:
pop rbp
ret
G_M1291_IG05:
call [System.ThrowHelper:ThrowArgumentOutOfRangeException()]
int3
; Total bytes of code: 96
在这里,JIT 使用了3个 ZMM(AVX-512)寄存器来执行类似于 memmove 的操作并内联(即使 src 和 dst 重叠)。对于编译时常量数据(例如 utf8 字面值),将生成类似的代码。
bool WriteHeader(Span<int> dst) => "text/html"u8.CopyTo(dst);
bool StartsWithHeader(Span<int> dst) => dst.StartsWith("text/html"u8);
一般优化
- PR#83911 在 NativeAOT 中,静态初始化现在更便宜。
- PR#84213 和 PR#84231 改善了 arr[arr.Length - cns] 和 arr[index % arr.Length] 模式的边界检查消除。
- 对于更多情况(如小类型)启用了前向替换优化,PR#83969。
- PR#85251 在寄存器分配期间改善了一些溢出情况。
- PR#84427 改善了 PGO 执行的可扩展性
- 我们继续改进 JIT 循环优化功能。在 Preview 4 中,我们改进了可达性集合的计算,PR#84204。
社区焦点(Lachlan Ennis)
我的名字是 Lachlan Ennis,我是 Expert1 的全栈软件开发人员,为澳大利亚的中小型金融公司编写软件。我住在澳大利亚昆士兰州的布里斯班市。我毕业于昆士兰科技大学(QUT),获得了信息技术学士学位,但我的大部分编程技能都是在工作中学到的。我们主要使用 .NET 和 MSSQL 编写软件。我们还为我们的产品使用 Winforms。
我对 .NET 的第一次贡献是在 dotnet/msbuild 中,我利用代码分析来提高代码质量和性能。这帮助 msbuild 在编码标准上更接近于 dotnet/runtime,并启用了代码分析规则。然后,当我偶然发现一个讨论 Winforms 互操作层以及如何改进它的问题时,我转向了 Winforms 的工作。我之前在 msbuild 中做过一些关于互操作的小工作,看到它可能会有多困难。我建议 Winforms 使用 CsWin32,因为它使用源代码生成器创建 PInvokes 以及友好的重载。这之后引发了 Microsoft/CsWin32 和 Microsoft/Win32Metadata 中许多问题和 PR,以便为 Winforms 提供所需的 API。
在处理互操作更改后,我转向 issue 队列,帮助调查积压的问题。通常,issue 需要有人推动调查,直到 Winforms 团队能够接管足够的信息,或者如果问题足够明显,则提交一个 PR 来修复这个 issue。我也会帮忙在社区中努力为 Winforms 代码库添加空注释。
在 dotnet 开源软件上工作确实有助于扩展我的 dotnet、C# 和 winforms 知识。在这方面,Winforms 团队帮助很大,他们在 PR 和 issue 中提供了详尽的审查和建议。
总结
.NET 8 预览版 4 包含了许多令人兴奋的新功能和改进,这些都离不开微软的一支多元化工程师团队以及充满激情的开源社区的辛勤工作和奉献。我们衷心感谢所有迄今为止为 .NET 8 做出贡献的人,无论是通过代码贡献、错误报告还是提供反馈。
您的贡献在制作 .NET 8 预览版中起到了关键作用,我们期待继续携手共建 .NET 和整个技术社区更加美好的未来。
想知道即将到来的是什么吗?来看看 .NET 接下来会有什么新变化!