該頁面可以包含自動翻譯的文字。

C# Native AOT 效能

與一般受控程式碼相比,.NET Native AOT 應用程式有多快?AOT 能否超越 JIT?如何為 Native AOT 應用程式做基準測試?

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 基準測試可讓你從效能角度比較不同設定。不過,結果取決於作業系統與處理器架構。你需要在目標環境中執行自己的基準測試,才能找出最佳部署設定。