该页面可以包含自动翻译的文本。
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 环境。
应用程序也在不同的配置中进行测试。某些配置中的应用程序定义了一个“场景”。
您可以按住 Ctrl(或 ⌘)键在 PowerBI 仪表板上选择多个场景或环境。
指标
基准测试收集每个已部署应用程序的基本指标。例如,测试测量每秒请求数 (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 候选版本(2024 年 9 月)的当前性能比较结果:
启动时间
AOT 应用程序的启动速度比托管版本快得多。对于 Stage1 和 Stage2 应用程序以及所有环境都是如此。示例结果:
场景 | 启动时间(毫秒) |
---|---|
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 存储库下载源代码。我们鼓励您在自己的环境中尝试它们。本文介绍了搭载 Intel Core i9-13900H 处理器和 16 GB RAM 的 Windows 11 笔记本电脑的结果。
确保正确运行基准测试。以下是常见建议:
- 使用发布版本。
- 关闭除基准测试过程之外的所有应用程序。例如,禁用防病毒软件,关闭 Visual Studio 和 Web 浏览器。
- 保持笔记本电脑插电并使用最佳性能模式。
- 在比较的场景中使用相同的输入数据。
测试用例
我们将在 .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 倍:
Method | Runtime | Mean | Error | StdDev |
---|---|---|---|---|
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 版本对于此文档来说稍快一些:
Method | Runtime | Mean | Error | StdDev |
---|---|---|---|---|
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。速度几乎相等:
Method | Runtime | Mean | Error | StdDev |
---|---|---|---|---|
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>
使用 benchmark.bat 脚本在 Windows 上运行测试。您可以将其转换为适用于 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,Native AOT 版本即使经过 100 次迭代也更快:
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
结论
Native 与常规 .NET 相比,AOT 应用程序启动速度更快。官方基准测试还显示,AOT 应用程序占用的内存更小。
但在启动后,托管应用程序通常会显示更好的速度。这是因为 JIT 可以访问运行时信息。在长时间运行的应用程序中,它可以基于动态配置文件引导优化和其他技术重新生成更有效的代码。
ASP.NET 基准测试允许您从性能角度比较不同的配置。但是,结果取决于操作系统和处理器架构。您需要在目标环境中运行自己的基准测试以找到最佳部署配置。