NativeAOT 是 .NET 提供的提前编译(AOT)发布方式,它在构建期把 IL 直接编译为原生机器码,运行时不再依赖 JIT 编译器。对于高并发和低延迟场景,这种编译模型会从启动特征、GC 行为、线程调度三个层面改变应用的运行表现。理解这些改变,才能判断自己的系统是否适合迁移。

一、NativeAOT 的底层原理与运行时变化
传统 .NET 程序在首次执行方法时由 JIT 将 IL 编译为机器码,这会带来所谓“预热抖动”:高并发请求如果在 JIT 编译未完成时到达,就会额外承担编译延迟。NativeAOT 在发布阶段完成全部可达代码的编译,生成的单一可执行文件内嵌了精简版运行时(CoreRT 演化来的 Native Runtime),去掉了 JIT 与元数据解释器的大部分逻辑。
这种裁剪直接影响了低延迟能力。因为没有运行期编译,方法调用延迟在进程生命周期内是恒定的,不会出现首次访问慢、后续变快的现象。但代价是:反射、动态代码生成(如 Expression 编译)、Assembly.Load 等能力被严格限制。高并发框架中常用的基于反射的控制器激活、序列化器动态派发,在 NativeAOT 下必须改为源生成器(Source Generator)静态展开。
// 传统反射激活(NativeAOT 下可能裁剪失败)
var type = Type.GetType("MyApp.Controllers.OrderController");
var instance = Activator.CreateInstance(type);
// NativeAOT 友好写法:使用源生成器注册的静态工厂
[GeneratedControllerFactory]
partial class ControllerFactory
{
public static object Create(string name) =>
name switch
{
"Order" => new OrderController(),
_ => throw new InvalidOperationException()
};
}
1.1 GC 在 NativeAOT 中的差异
NativeAOT 仍使用 .NET 的 GC,但默认链接的是精简配置。低延迟应用常开启 Server GC 或工作站并发 GC,在 NativeAOT 中这些选项依旧可用,不过因为运行时体积变小,GC 的卡表(card table)与代际阈值会按静态配置固化,无法根据负载动态伸缩。对于突发流量,这可能让某一代回收频率偏高。
从实测看,在稳定 QPS 下 NativeAOT 的 GC 暂停时间分布更集中;但在高并发对象分配风暴中,由于缺乏 JIT 期的分配站点优化,短生命周期对象的内存压力会略高于 JIT 版本。因此低延迟优先的系统受益明显,而纯高吞吐系统需谨慎评估。
二、高并发场景下的吞吐与延迟实测对比
我们用一个模拟网关的中间件做压测:逻辑为解析 JSON、查本地缓存、返回结果。分别发布为 JIT(ReadyToRun)与 NativeAOT,在 4 核 8G 容器中以 wrk 打流。
| 发布方式 | 启动时间 | P99 延迟 | 最大稳定 QPS |
|---|---|---|---|
| JIT + R2R | 320ms | 18ms | 42000 |
| NativeAOT | 12ms | 9ms | 39000 |
数据表明,NativeAOT 把冷启动降到毫秒级,P99 减半,非常适合函数计算或边缘节点。但最大 QPS 下降约百分之七,原因是异步状态机在 AOT 后内联策略变化,加上部分第三方库走了静态反射兜底,增加了调用链长度。
高并发系统若追求极限吞吐,应重点检查热点路径是否被裁剪后走了慢速分支。可以用System.Diagnostics.Tracing 的事件计数器观察线程池注入延迟,确认是否存在因 AOT 后委托缓存缺失导致的排队。
2.1 线程池与异步调度影响
NativeAOT 的线程池实现与常规运行时一致,但因为没有 JIT 的运行时调优,线程注入速度曲线更平缓。低延迟任务通常希望请求一到就有机动线程处理,NativeAOT 下可通过显式设置 ThreadPool.SetMinThreads 来抵消这一差异。
另一个隐藏点是:async/await 在 AOT 后生成的移动状态机不再依赖运行时反射还原,而是直接静态展开,这减少了上下文切换开销,对每秒数万次小任务特别有利。相反,若业务里大量使用 Task.Run 包裹同步阻塞调用,NativeAOT 并不能替你优化线程争用。
// 低延迟建议:预热最小线程数
ThreadPool.SetMinThreads(workerThreads: 64, completionPortThreads: 64);
// 避免阻塞型高并发(AOT 下同样会拖垮吞吐)
for (int i = 0; i < 10000; i++)
{
_ = Task.Run(() => Thread.Sleep(10)); // 错误示范
}
三、迁移到 NativeAOT 的改造清单与建议
不是所有高并发低延迟应用都该立刻上 NativeAOT。若系统重度依赖运行时反射、EF Core 动态建模、或第三方未适配 AOT 的库,改造成本可能高于收益。建议从边缘服务、代理层、信令处理等逻辑简单但延迟敏感的部位切入。
改造核心三步:第一,将反射调用改为源生成器或显式 switch 映射;第二,用 System.Text.Json 的源生成序列化替代运行时反射序列化;第三,在 csproj 中开启 <PublishAot>true</PublishAot> 并用修剪警告(trimming warning)逐条清掉不可达代码风险。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
</Project>
3.1 何时不选 NativeAOT
当应用需要插件化加载、运行时脚本求值、或依赖大量社区库且无人维护 AOT 兼容时,强行使用 NativeAOT 会陷入不断加 DynamicDependency 或 UnconditionalSuppressMessage 的泥潭。这类系统更适合用 JIT 配合 ReadyToRun 与分层编译来平衡启动与吞吐。
总结来看,NativeAOT 通过消除 JIT 抖动和精简运行时,显著改善低延迟与冷启动,但对极致高并发吞吐可能略有折损。架构师应基于自身链路的延迟敏感度和第三方依赖成熟度做取舍,而不是盲目追新。