このページには自動翻訳されたテキストを含めることができます。

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 環境もあります。

アプリケーションは、さまざまな構成でもテストされます。ある構成のアプリケーションが 1 つの「シナリオ」を定義します。

PowerBI ダッシュボードでは、Ctrl(または ⌘)キーを押しながら、複数のシナリオまたは環境を選択できます。

メトリック

ベンチマークは、デプロイ済みの各アプリケーションについて基本的なメトリックを収集します。たとえば、テストでは 1 秒あたりのリクエスト数(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 シナリオも確認できます。このようなシナリオは trimmed ReadyToRun デプロイメントに対応しており、これは .NET における別の形式の事前コンパイルです。場合によっては、Native AOT のよい代替になります。

以下は、.NET 9 リリース候補(2024 年 9 月)の現在のパフォーマンス比較結果です。

起動時間

AOT アプリケーションは、管理版よりもかなり速く起動します。これは Stage1 と Stage2 の両方のアプリケーション、およびすべての環境で当てはまります。サンプル結果は次のとおりです。

シナリオ 起動時間 (ms)
Stage2AotSpeedOpt 100
Stage2Aot 109
Stage2 528

ワーキング セット

Native AOT アプリケーションの最大ワーキング セットは、管理版より小さくなります。Linux では、管理版は AOT 版より約 1.5〜2 倍多く RAM を使用します。たとえば、次のとおりです。

シナリオ 最大ワーキング セット (MB)
Stage1Aot 56
Stage1AotSpeedOpt 57
Stage1 126

Windows では差は小さくなります。特に Stage2 ではその傾向が顕著です。

シナリオ 最大ワーキング セット (MB)
Stage2Aot 152
Stage2AotSpeedOpt 150
Stage2 167

毎秒リクエスト数

RPS 値が大きいほど、アプリケーションは高速です。軽量な Stage1 アプリケーションは、通常 1 秒あたり約 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 種類のベンチマークを使用します。1 つ目は、.NET コードのベンチマーク用として広く使われている BenchmarkDotNet に基づくものです。これらのベンチマークは、起動時間を除いた純粋な速度を比較します。

2 つ目は hyperfine ツールに基づくものです。2 つのシェルコマンドの実行時間を比較できます。これらのベンチマークは、起動時間を含む総合的な速度を比較します。

ここではメモリ消費は比較しません。現時点では、BenchmarkDotNet の NativeMemoryProfiler 診断器は Native AOT ランタイムをサポートしていません。hyperfine も現在はメモリ使用量を追跡しません。

ソースコードは GitHub の NativeAotBenchmarks リポジトリからダウンロードできます。ぜひ環境で試してみてください。この記事では、Intel Core i9-13900H プロセッサと 16 GB RAM を搭載した Windows 11 ノート PC での結果を示します。

ベンチマークは適切に実行してください。一般的な推奨事項は次のとおりです。

  • Release ビルドを使用します。
  • ベンチマーク プロセス以外のアプリケーションはすべて終了します。たとえば、ウイルス対策ソフトを無効化し、Visual Studio と Web ブラウザーを閉じます。
  • ノート PC は電源に接続し、最高パフォーマンス モードを使用します。
  • 比較するシナリオでは同じ入力データを使用します。

テストケース

.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 はコードを事前コンパイルし、いくつかの統計を収集できます。そのため、このような benhmarks では起動時間は比較対象から除外されます。

文字列圧縮

文字列圧縮の 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 はベンチマーク結果を歪める可能性があります。

2 つの PDF ドキュメントで速度をテストします。1 つ目の 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

2 つ目の文書はより小さく、より単純です。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 に 1 回変換する場合、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 がランタイム情報にアクセスできるためです。長時間実行されるアプリケーションでは、動的な profile-guided optimization やその他の手法に基づいて、より効果的なコードを再生成できます。

ASP.NET ベンチマークを使うと、パフォーマンスの観点からさまざまな構成を比較できます。ただし、結果はオペレーティング システムとプロセッサ アーキテクチャに依存します。最適なデプロイメント構成を見つけるには、対象環境で独自のベンチマークを実行する必要があります。