.NET 的 Native AOT 是什么 - 先厘清与 JIT、ReadyToRun、trimming 的区别

· · C#, .NET, Native AOT, 发布, 设计

之前写过 把 C# 通过 Native AOT 做成原生 DLL 的方法 - 使用 UnmanagedCallersOnly 从 C/C++ 调用,但那篇其实顺序有点颠倒了。
本来应该先讲「Native AOT 到底是什么」比较自然。

Native AOT 这个话题,一开始这几点常常会混在一起。

  • 是在说去掉 JIT 这件事吗
  • 和 self-contained 或 single-file 有什么不同
  • 和 ReadyToRun 是一类东西吗
  • trimming warning 大量冒出来到底是什么情况
  • WPF 或 WinForms 能不能直接套上去

这些混在一起的时候,Native AOT 看起来会像「只是变快的魔法」,或者反过来像「全是限制、不能碰的东西」。
两边都稍显粗糙。

简单说,Native AOT 是 把 .NET 应用在 publish 时相当彻底地固化下来的发布模式
代价是对启动和发布层面效果明显。
反过来,想在运行期动态解决各种事情的程序,就不太合拍。

这篇文章就从实务角度整理这一点。

1. Native AOT 做了什么

平时的 .NET 会先生成 IL,再在运行期对需要的地方做 JIT。
Native AOT 则是把「运行期才生成原生代码」这一步大幅提前到 publish 阶段。

所以从外观上看,它是朝着「把 .NET 打包得像原生应用一样发布」的方向走。

这里重要的是 希望在 publish 时就能看到绝大多数必要的代码

也就是说,

  • 运行期才去查找类型
  • 运行期才生成代码
  • 运行期才读取程序集来判断

这类写法,无论如何都会比较不合适。

如果把 Native AOT 只看成「变快的功能」,这里就会理解偏了。
本质上更像是 舍弃一部分动态世界,往容易固化的静态世界靠拢

2. 和 ReadyToRun 差在哪里

先把这里分清楚,会轻松很多。

角度 一般 JIT ReadyToRun Native AOT
运行期 JIT 使用 仍会使用 不使用
发布产物 以 IL 为主 IL + 事先生成的原生代码 以原生可执行文件为主
启动 基准 容易改善 更容易改善
兼容性 广 相对广 限制较多

ReadyToRun 是把 JIT 的工作稍微提前的方向。
Native AOT 则是 不以运行期 JIT 为前提 的方向。

名字看起来接近,实际手感却差不少。
ReadyToRun 是「先让启动稍微轻一点」。
Native AOT 是「为了启动、发布、运行环境的需求,设计也往静态一点靠」。

3. 用了 Native AOT 有什么好处

最直观的还是启动速度。

在 CLI 工具、短生命周期进程、容器里的小型 API、常驻但功能有限的工具上,JIT 的开销会很明显。
Native AOT 把这部分提前做完,起步就会更轻。

另一个是发布层面。
在不想要求「请先在机器上安装 .NET Runtime」的场景中,心里会轻松不少。

  • 只想发布一个小工具
  • 希望容器镜像尽量小
  • 不想在运行环境上装 JIT

这种场景下,Native AOT 用得上的位置很明确。

所以 Native AOT 适合的不是「所有应用」,而是 想减少启动、发布、运行环境前提条件的应用

4. 反过来哪些地方会变辛苦

这里答案相当明确。
首先是反射与动态生成代码。

  • Assembly.LoadFile
  • System.Reflection.Emit
  • 运行期把类型逐一枚举出来查找的结构
  • 从字符串名称查类型再实例化的机制

这些在 publish 时不容易把必要代码固定下来,就会成为 AOT warning 的温床。

Native AOT 冒出一堆 warning 时,与其觉得「太吵了」,不如读成「这个设计在 publish 时就是看不清楚」。
尤其是 RequiresDynamicCode 系列,通常值得认真看一看。

再来是 trimming。
Native AOT 和 trimming 绑得相当紧。
这里麻烦的是,不只自己的代码,连依赖库的写法都会受影响。

例如,

  • 以反射为基础的序列化器
  • 以运行期扫描为前提的 DI
  • 使用动态代理的库

这些都容易让事情变麻烦。

换句话说,做 Native AOT 的时候,与其「运行期再聪明处理」,不如 往编译期就能看清楚的形式靠拢 更好控制。

5. Windows 桌面端要稍微谨慎一点

在 Windows 上,Native AOT 没有内置 COM 支持。
再加上 WPF 和 trimming 的搭配并不好,WinForms 也相当依赖内置的 COM marshalling。

因此,

  • 直接把 WPF / WinForms 主体丢给 Native AOT
  • 照常把 COM interop 直接带进去

这些都要谨慎对待。

反过来,比较适合当入口的是,

  • Console
  • worker
  • 小型 Web API
  • 职责单一的小组件

Native AOT 并不是「.NET 的东西都能直接套上去」。
尤其在 Windows 桌面和 COM 味浓的世界中,很多场景还是维持一般 JIT 会更顺畅。

6. 最小步骤其实很简单

在项目端,先在 csproj 里加上 PublishAot

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

publish 的命令,例如 Windows x64 会是这样。

dotnet publish -c Release -r win-x64

Linux x64 则是,

dotnet publish -c Release -r linux-x64

走到这一步,就已经相当「像原生应用的世界」了。
由于 RID 是固定的,产物比起「同一个 DLL 到哪都能跑」,更像是为该 OS / 架构量身定制的可执行文件。

另外还有一个意外常中招的地方是 JSON。
与其用平常的方式把 System.Text.Json 写成依赖反射的路径,不如靠 source generation,更不容易出问题。

using System.Text.Json;
using System.Text.Json.Serialization;

[JsonSerializable(typeof(AppConfig))]
internal partial class AppJsonContext : JsonSerializerContext
{
}

public sealed class AppConfig
{
    public string? Name { get; init; }
    public int RetryCount { get; init; }
}

var config = new AppConfig
{
    Name = "sample",
    RetryCount = 3
};

string json = JsonSerializer.Serialize(config, AppJsonContext.Default.AppConfig);

与其说是为了 Native AOT,不如记成 改往「不要让运行期去查找类型」的写法靠,这样比较不会理解偏。

7. 从哪里开始试比较好

第一个尝试对象,下列几类比较容易上手。

  • 启动速度很重要的 CLI
  • worker
  • 小型 Web API
  • 在原生集成中职责较窄的组件

这些比较容易减少动态机制,也比较容易看到 Native AOT 的好处。

反之,WPF / WinForms 既有的大型应用主体、以插件加载为核心的结构、重度依赖 COM 的结构,作为首批尝试对象并不太平顺。

8. 总结

Native AOT 是在 publish 时把 .NET 做得相当静态的机制。

  • 对启动有效
  • 发布也会变轻松
  • 代价是对动态机制会变得严格

先掌握这三点,思路会清楚很多。

Native AOT 不是「所有 .NET 应用都该套上的标配开关」。
但在启动速度重要、想让发布更轻松、想降低对运行环境前提条件的要求时,它是相当强的选项。

反过来,在 Windows 桌面或 COM 味浓的世界中,继续走一般 .NET 往往更顺畅。
能分清楚这两边之后,Native AOT 就不再是「难搞的新功能」,而是位置清晰的工具。

9. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

伪随机数与真随机数的区别 - 如何区分的整理

本文整理伪随机数与真随机数的区别,重点不在输出外观而在生成器结构:普通 PRNG 重视可重现性、CSPRNG / DRBG 主打不可预测性、NRBG 则以物理熵源为基础。文中说明种子(seed)与重新播种(reseed)、健康检测(health test)的作用,并给出信息...

常见问题

汇总了咨询这一主题时常见的问题。

Native AOT 是什么?
Native AOT 是把 .NET 应用在 publish 时相当彻底地固化下来的发布模式。平时的 .NET 会先生成 IL,再在运行期对需要的地方做 JIT;Native AOT 则把「运行期才生成原生代码」这一步大幅提前到 publish 阶段,产出以原生可执行文件为主的成品,不再使用运行期 JIT。它的本质不是单纯「变快的魔法」,而是舍弃一部分动态世界,往容易固化的静态世界靠拢,对启动和发布层面的效果很明显。
Native AOT 和 ReadyToRun 有什么区别?
ReadyToRun 是把 JIT 的工作稍微提前的方向,发布产物是 IL 加上事先生成的原生代码,运行期仍会用到 JIT,兼容性相对广。Native AOT 则是不以运行期 JIT 为前提的方向,发布产物以原生可执行文件为主,启动更容易改善但限制更多。简单说,ReadyToRun 是「先让启动稍微轻一点」,Native AOT 是「为了启动、发布、运行环境的需求,设计也往静态靠一点」。
什么样的应用适合 Native AOT?
适合的是想减少启动、发布、运行环境前提条件的应用,例如启动速度很重要的 CLI 工具、worker、容器里的小型 Web API、职责单一的小组件。它在不想要求运行环境事先安装 .NET Runtime、希望容器镜像尽量小的场景特别有用。反过来,重度依赖反射与动态生成代码的结构、以插件加载为核心的结构,就不太合拍。
WPF 或 WinForms 可以用 Native AOT 吗?
要谨慎对待。在 Windows 上 Native AOT 没有内置 COM 支持,而 WPF 和 trimming 的搭配并不好,WinForms 也相当依赖内置的 COM marshalling,所以直接把 WPF / WinForms 主体丢给 Native AOT、或者照常把 COM interop 带进去,都不建议。比较适合当入口的是 Console、worker、小型 Web API 这类职责清晰的组件;Windows 桌面和 COM 味浓的世界,很多场景维持一般 JIT 会更顺畅。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表