該頁面可以包含自動翻譯的文字。
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 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 應用程式通常可處理約 80 萬到 90 萬次請求/秒。較大的 Stage2 應用程式只能處理約 20 萬次請求。
對於 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 筆記型電腦上的結果。
請確保正確執行基準測試。以下是常見建議:
- 使用 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. 較重的 PDF 轉 PNG 轉換 任務,使用 Docotic.Pdf。
先決條件
安裝 必要條件 以進行 .NET Native AOT 部署。
安裝 hyperfine 以執行對應的基準測試。
對於 PDF 轉 PNG 基準測試,請在 下載 C# .NET PDF 程式庫 頁面取得免費的限時授權金鑰。你需要在 Helper.cs 中套用該授權金鑰。
BenchmarkDotNet
這些基準測試位於 NativeAotBenchmarks 專案中。我們比較 RuntimeMoniker.NativeAot80 與 RuntimeMoniker.Net80 的結果。預設情況下,BenchmarkDotNet 會使用 OptimizationPreference=Speed 設定建置 Native AOT 程式碼。
BenchmarkDotNet 會執行 6 次或更多次 warmup 迭代。這有助於 JIT 預先編譯程式碼並收集一些統計資料。因此,這類 benhmarks 不會將啟動時間納入比較。
字串壓縮
字串壓縮的 CompressString 基準測試使用一個包含重複字元的長字串。常見錯誤是產生隨機字串。在那種情況下,Native AOT 與 .NET 8 的基準測試會使用不同的輸入字串。可以使用隨機字串,但你需要用相同的 seed 初始化隨機產生器。
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 的 warmup 執行有助於在「熱」磁碟快取上啟動測試應用程式。與 BenchmarkDotNet 不同,hyperfine 的 warmup 不會幫助 JIT。因此,hyperfine 基準測試比較的是總體應用程式速度,包括啟動時間。
我們的測試應用程式支援 iteration count 參數。它可以在簡單迴圈中重複執行相同的程式碼多次:
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
結論
Native AOT 應用程式的啟動速度比一般 .NET 更快。官方基準測試也顯示,AOT 應用程式的記憶體占用更小。
但在啟動之後,受控應用程式通常表現出更好的速度。這是因為 JIT 可以存取執行階段資訊。在長時間執行的應用程式中,它可以根據動態設定檔導向最佳化與其他技術,重新產生更有效率的程式碼。
ASP.NET 基準測試可讓你從效能角度比較不同設定。不過,結果取決於作業系統與處理器架構。你需要在目標環境中執行自己的基準測試,才能找出最佳部署設定。