.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.LoadFileSystem.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. 参考资料
- Native AOT deployment overview - .NET
- Native AOT deployment overview - .NET (日本語)
- Introduction to AOT warnings - .NET
- Prepare .NET libraries for trimming - .NET
- Known trimming incompatibilities - .NET
- How to use source generation in System.Text.Json - .NET
- ASP.NET Core support for Native AOT
- ReadyToRun deployment overview - .NET
- Building native libraries - .NET
- ComWrappers source generation - .NET
- 相关文章:把 C# 通过 Native AOT 做成原生 DLL 的方法 - 使用 UnmanagedCallersOnly 从 C/C++ 调用
- 相关文章:要在 C# 中使用原生 DLL,C++/CLI 包装是有力选项的理由 - 与 P/Invoke 的对比整理
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
PeriodicTimer / System.Threading.Timer / DispatcherTimer 怎么选 - 先理清 .NET 的定期执行
整理 .NET 中 PeriodicTimer、System.Threading.Timer、DispatcherTimer 的区别与使用场景,从线程、async 流程、callback 重叠三个角度切入,帮助你在 worker、ThreadPool 后台处理及 WPF U...
将 .NET Framework 迁移到 .NET 之前该确认的事 - 动手前就决定成败的实战检查清单
整理将 .NET Framework 业务应用程序迁移到现代 .NET 之前必须先盘点的论点。涵盖落地版本、Windows 专用前提的取舍、不再支持的 API、共享库切分方式、第三方组件、运维与 CI/CD,帮助在动手前厘清范围并降低迁移风险。
Windows 什么时候需要管理员权限 - UAC、保护区域与设计上的辨别方式
从边界与存储位置的角度,整理 Windows 什么时候真正需要管理员权限:UAC、保护区域、HKLM、服务、驱动、防火墙。同时说明 per-user 与 per-machine 的差异,以及把管理员处理拆成独立 EXE、服务或任务的设计取舍,帮读者判断该不该提升权限。
Windows Forms、WPF、WinUI 该怎么选 - 新建项目、存量资产、发布、UI 表现力判断表
从存量资产的规模、界面是表单为主还是需要表现力、现代 Windows UI 是否是产品刚需、发布与运维怎么落地这四个角度,整理 WinForms、WPF、WinUI 该怎么选的判断表,并提醒只想用 Windows App SDK 不必全面迁移到 WinUI。
伪随机数与真随机数的区别 - 如何区分的整理
本文整理伪随机数与真随机数的区别,重点不在输出外观而在生成器结构:普通 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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。