该页面可以包含自动翻译的文本。
C# Native AOT 性能
与常规托管代码相比,.NET Native AOT 应用程序有多快?AOT 能超越 JIT 吗?如何对 Native AOT 应用程序进行基准测试?

本文是 .NET 中 Native AOT 系列文章的一部分。如果你不熟悉 Native AOT,请先阅读 如何在 .NET 中开发 Native AOT 应用程序 部分。
本文比较 .NET 和 Native AOT 的性能。首先,我们将回顾 Microsoft 的官方基准测试。它们可以比较简单 ASP.NET 应用程序的不同 .NET 部署选项。
然后,你将学习如何使用 BenchmarkDotNet 和 hyperfine 工具运行自己的基准测试。这类基准测试可以让你衡量代码在你的环境中的执行速度。
ASP.NET 基准测试
ASP.NET 团队维护了一套完善的性能测试基础设施。他们在不同环境中测试各种场景。
我们最关注 Native AOT 基准测试。最主要的信息来源是下面的 PowerBI 仪表板。其中的数据基于 3 个“支柱”:测试应用程序、部署场景和指标。
测试应用程序
你可以在 aspnet/Benchmarks 仓库中找到基准测试和测试应用程序的源代码。
Native AOT 基准测试比较 3 种应用程序类型:
- Stage1 - 基于 HTTP 和 JSON 的最小 API。应用程序源代码位于
/src/BenchmarksApps/BasicMinimalApi。 - Stage1Grpc - 一个基于 gRPC 的类似 API(
/src/BenchmarksApps/Grpc/BasicGrpc) - Stage2 - 涉及数据库、身份验证的完整 Web 应用程序(
/src/BenchmarksApps/TodosApi)
.NET 部署场景
测试应用程序在不同环境中运行。目前,基准测试使用配备 28 个核心的 Windows 和 Linux 虚拟机。还存在针对 ARM 和 Intel 处理器的独立 Linux 环境。
应用程序也会在不同配置下进行测试。某种配置下的应用程序定义了一个“场景”。
你可以在 PowerBI 仪表板上按住 Ctrl(或 ⌘)键来选择多个场景或环境。
指标
基准测试会为每个已部署的应用程序收集基础指标。例如,测试会测量每秒请求数(RPS)、启动时间、最大内存工作集。
这使我们可以比较同一应用程序在不同配置下的指标值。
性能比较
我们将比较 StageX 场景与 StageXAot 和 StageXAotSpeedOpt。它们使用以下配置:
| 场景 | dotnet publish 构建参数 |
|---|---|
| StageX | PublishAot=false EnableRequestDelegateGenerator=false |
| Stage2Aot | PublishAot=true StripSymbols=true |
| Stage2AotSpeedOpt | PublishAot=true StripSymbols=true OptimizationPreference=Speed |
以上所有场景还使用了 DOTNET_GCDynamicAdaptationMode=1 环境变量。
StageXAotSpeedOpt 场景可用于评估 OptimizationPreference = Speed 设置的影响。
你也可以查看 StageXTrimR2RSingleFile 场景。这类场景对应裁剪后的 ReadyToRun 部署,它是 .NET 中另一种提前编译形式。有时,它是 Native AOT 的一个不错替代方案。
以下是 .NET 9 Release Candidate(2024 年 9 月)的当前性能比较结果:
启动时间
AOT 应用程序的启动速度比托管版本快得多。对于 Stage1 和 Stage2 应用程序以及所有环境都是如此。示例结果:
| 场景 | 启动时间 (ms) |
|---|---|
| Stage2AotSpeedOpt | 100 |
| Stage2Aot | 109 |
| Stage2 | 528 |
工作集
Native AOT 应用程序的最大工作集小于托管版本。在 Linux 上,托管版本使用的 RAM 大约是 AOT 版本的 1.5 - 2 倍。例如:
| 场景 | 最大工作集 (MB) |
|---|---|
| Stage1Aot | 56 |
| Stage1AotSpeedOpt | 57 |
| Stage1 | 126 |
在 Windows 上,差异更小,尤其是 Stage2:
| 场景 | 最大工作集 (MB) |
|---|---|
| Stage2Aot | 152 |
| Stage2AotSpeedOpt | 150 |
| Stage2 | 167 |
每秒请求数
更大的 RPS 值意味着更快的应用程序。轻量级的 Stage1 应用程序通常每秒可处理约 800-900K 个请求。较大的 Stage2 应用程序只能处理约 200K 个请求。
对于 Stage2 应用程序,.NET 版本在所有环境中的请求处理量都高于 AOT 版本。Stage2AotSpeedOpt 版本的速度有时接近,但通常介于 Stage2 和 Stage2Aot 之间。典型结果如下:
| 场景 | RPS |
|---|---|
| Stage2 | 235,008 |
| Stage2AotSpeedOpt | 215,637 |
| Stage2Aot | 194,264 |
Stage1 应用程序的结果在 Intel Linux 和 Intel Windows 上类似。不过,在 Ampere Linux 上,AOT 优于托管版本。以下是 Ampere Linux 的示例结果:
| 场景 | RPS |
|---|---|
| Stage1AotSpeedOpt | 929,524 |
| Stage1Aot | 912,344 |
| Stage1 | 844,659 |
因此,环境和应用程序代码会显著影响速度。运行自己的基准测试来评估 Native AOT 对项目的收益是合理的。下面我们在没有 Microsoft 测试基础设施的情况下编写自定义基准测试。
Native AOT 应用程序基准测试
我们将使用 2 种基准测试。第一种基于 BenchmarkDotNet——用于 .NET 代码基准测试的流行库。这类基准测试比较纯粹的速度,不包括启动时间。
第二种基于 hyperfine 工具。它可以比较两个 shell 命令的执行时间。这类基准测试比较整体速度,包括启动时间。
这里我们不比较内存占用。目前,BenchmarkDotNet 中的 NativeMemoryProfiler 诊断器不支持 Native AOT 运行时。hyperfine 目前也不会跟踪内存使用情况。
你可以从 GitHub 上的 NativeAotBenchmarks 仓库下载源代码。我们鼓励你在自己的环境中尝试它们。本文描述的是一台 Windows 11 笔记本电脑上的结果,该机器搭载 Intel Core i9-13900H 处理器和 16 GB 内存。
请确保正确运行基准测试。以下是常见建议:
- 使用 Release 构建。
- 关闭除基准测试进程之外的所有应用程序。例如,禁用杀毒软件,关闭 Visual Studio 和网页浏览器。
- 保持笔记本电脑接通电源,并使用最佳性能模式。
- 在比较的场景中使用相同的输入数据。
测试用例
我们将在 .NET 8 中测试 2 个场景:
1. 用于基于重复字符计数的字符串压缩的简单 C# 代码。例如,字符串 "aabcccccaaa" 会变成 "a2b1c5a3":
string Compress(string s)
{
StringBuilder compressed = new(s.Length);
for (int i = 0; i < s.Length; ++i)
{
char c = s[i];
for (int j = i + 1; j <= s.Length; ++j)
{
if (j == s.Length || s[j] != c)
{
compressed.Append(c + $"{j - i}");
i = j - 1;
if (compressed.Length > s.Length)
return s;
break;
}
}
}
if (compressed.Length <= s.Length)
return compressed.ToString();
return s;
}
2. 使用 Docotic.Pdf 的更重的 PDF 转 PNG 转换 任务。
先决条件
为 .NET Native AOT 部署安装 先决条件。
安装 hyperfine 以运行相应的基准测试。
对于 PDF 转 PNG 基准测试,请在 下载 C# .NET PDF 库 页面获取一个免费的限时许可证密钥。你需要在 Helper.cs 中应用该许可证密钥。
BenchmarkDotNet
这些基准测试位于 NativeAotBenchmarks 项目中。我们比较 RuntimeMoniker.NativeAot80 和 RuntimeMoniker.Net80 的结果。默认情况下,BenchmarkDotNet 会使用 OptimizationPreference=Speed 设置构建 Native AOT 代码。
BenchmarkDotNet 会执行 6 次或更多预热迭代。这有助于 JIT 预编译代码并收集一些统计信息。因此,这类基准测试会将启动时间排除在比较之外。
字符串压缩
字符串压缩的 CompressString 基准测试使用一个包含重复字符的长字符串。常见错误是生成随机字符串。在这种情况下,Native AOT 和 .NET 8 的基准测试会使用不同的输入字符串。可以使用随机字符串,但你需要用相同的种子初始化随机生成器。
Native AOT 版本运行速度约比 .NET 8 版本快 1.08 倍:
| 方法 | 运行时 | 平均值 | 误差 | 标准差 |
|---|---|---|---|---|
| Compress | .NET 8.0 | 4.117 ms | 0.0553 ms | 0.0517 ms |
| Compress | NativeAOT 8.0 | 3.809 ms | 0.0403 ms | 0.0377 ms |
PDF 转 PNG
PDF 转 PNG 基准测试在内存中处理 PDF 文档。这样可以排除与文件系统的交互。磁盘上的 I/O 操作会使基准测试结果产生偏差。
我们使用两个 PDF 文档测试速度。第一个 Banner Edulink One.pdf 更复杂。它会转换为 72 dpi PNG,处理所需时间更长。对于该文档,.NET 8 版本稍快一些:
| 方法 | 运行时 | 平均值 | 误差 | 标准差 |
|---|---|---|---|---|
| Convert | .NET 8.0 | 1.103 s | 0.0156 s | 0.0146 s |
| Convert | NativeAOT 8.0 | 1.167 s | 0.0160 s | 0.0149 s |
第二个文档更小、更简单。它会转换为 300 dpi PNG,速度几乎相同:
| 方法 | 运行时 | 平均值 | 误差 | 标准差 |
|---|---|---|---|---|
| Convert | .NET 8.0 | 290.1 ms | 5.78 ms | 6.88 ms |
| Convert | NativeAOT 8.0 | 288.3 ms | 4.44 ms | 3.94 ms |
hyperfine
这些基准测试位于 NativeAotTestApp 项目中。该项目不使用 OptimizationPreference=Speed 设置。你可以在 NativeAotTestApp.csproj 中启用它:<OptimizationPreference>Speed</OptimizationPreference>
在 Windows 上使用 benchmark.bat 脚本运行测试。你可以将其转换为适用于 Unix/Linux 系统的 Bash 脚本。该脚本会构建同一应用程序的 .NET 8 和 Native AOT 版本。然后,它使用类似命令比较它们的性能:hyperfine --warmup 3 "net8-app.exe" "native-aot-app.exe"
hyperfine 中的预热运行有助于在“热”磁盘缓存上启动测试应用程序。与 BenchmarkDotNet 不同,hyperfine 的预热不会帮助 JIT。因此,hyperfine 基准测试比较的是应用程序总速度,包括启动时间。
我们的测试应用程序支持迭代次数参数。它允许在简单循环中多次重复相同代码:
for (int i = 0; i < iterationCount; ++i)
CompressString(args);
其思路是降低启动时间差异的影响。重复相同代码会给 JIT 机会收集更多运行时统计信息并生成更快的代码。
常见情况如下。第一次使用单次迭代运行基准测试时,Native AOT 版本快很多。然后,当你使用多次迭代运行相同的基准测试时,两个版本的总速度变得相同。这意味着在启动之后,托管版本实际上更快。
字符串压缩
对于相同输入字符串压缩的 100,000 次迭代,Native AOT 性能更好:
Benchmark 1: .NET 8 version (100000 iterations)
Time (mean ± σ): 151.5 ms ± 2.6 ms [User: 32.1 ms, System: 1.6 ms]
Range (min … max): 148.0 ms … 157.5 ms 19 runs
Benchmark 2: Native AOT version (100000 iterations)
Time (mean ± σ): 55.1 ms ± 3.1 ms [User: 15.0 ms, System: 2.1 ms]
Range (min … max): 51.6 ms … 65.9 ms 51 runs
Summary
Native AOT version ran 2.75 ± 0.16 times faster than .NET 8 version
但在 10,000,000 次迭代时,速度几乎相同:
Benchmark 1: .NET 8 version (10000000 iterations)
Time (mean ± σ): 3.984 s ± 0.139 s [User: 2.946 s, System: 0.009 s]
Range (min … max): 3.790 s … 4.182 s 10 runs
Benchmark 2: Native AOT version (10000000 iterations)
Time (mean ± σ): 3.956 s ± 0.041 s [User: 2.848 s, System: 0.004 s]
Range (min … max): 3.888 s … 4.016 s 10 runs
Summary
Native AOT version ran 1.01 ± 0.04 times faster than .NET 8 version
PDF 转 PNG
对于 Banner Edulink One.pdf 到 PNG 的单次转换,AOT 版本运行速度约比 .NET 8 版本快 1.88 倍:
Benchmark 1: .NET 8 version (1 iteration)
Time (mean ± σ): 2.417 s ± 0.104 s [User: 1.334 s, System: 0.116 s]
Range (min … max): 2.295 s … 2.629 s 10 runs
Benchmark 2: Native AOT version (1 iteration)
Time (mean ± σ): 1.288 s ± 0.011 s [User: 0.573 s, System: 0.123 s]
Range (min … max): 1.274 s … 1.310 s 10 runs
对于 20 次迭代,速度差异可以忽略不计:
Benchmark 1: .NET 8 version (20 iterations)
Time (mean ± σ): 25.048 s ± 0.223 s [User: 13.278 s, System: 2.312 s]
Range (min … max): 24.751 s … 25.423 s 10 runs
Benchmark 2: Native AOT version (20 iterations)
Time (mean ± σ): 25.213 s ± 0.114 s [User: 12.661 s, System: 2.275 s]
Range (min … max): 25.042 s … 25.350 s 10 runs
Summary
.NET 8 version ran 1.01 ± 0.01 times faster than Native AOT version
对于 3BigPreview.pdf,即使在 100 次迭代下,Native AOT 版本也更快:
Benchmark 1: .NET 8 version (100 iterations)
Time (mean ± σ): 10.009 s ± 0.152 s [User: 5.298 s, System: 0.567 s]
Range (min … max): 9.677 s … 10.189 s 10 runs
Benchmark 2: Native AOT version (100 iterations)
Time (mean ± σ): 8.336 s ± 0.070 s [User: 3.405 s, System: 0.505 s]
Range (min … max): 8.247 s … 8.459 s 10 runs
Summary
Native AOT version ran 1.20 ± 0.02 times faster than .NET 8 version
结论
与常规 .NET 相比,Native AOT 应用程序启动更快。官方基准测试也显示,AOT 应用程序具有更小的内存占用。
但在启动之后,托管应用程序通常表现出更好的速度。这是因为 JIT 可以访问运行时信息。在长时间运行的应用程序中,它可以基于动态配置文件引导优化和其他技术重新生成更有效的代码。
ASP.NET 基准测试允许你从性能角度比较不同配置。不过,结果取决于操作系统和处理器架构。你需要在目标环境中运行自己的基准测试,以找到最优的部署配置。