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

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

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

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 シナリオも確認してください。このようなシナリオは、.NET の事前コンパイルの別の形式であるトリミングされた ReadyToRun 展開に対応しています。場合によっては、Native AOT の優れた代替手段となります。

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

起動時間

AOT アプリケーションは、マネージド バージョンよりもはるかに速く起動します。これは、Stage1 と Stage2 の両方のアプリケーション、およびすべての環境に当てはまります。サンプル結果:

シナリオ 起動時間 (ミリ秒)
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

1 秒あたりのリクエスト数

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

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

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

ソース コードは、GitHub の NativeAotBenchmarks リポジトリからダウンロードできます。お使いの環境で試してみることをお勧めします。この記事では、Intel Core i9-13900H プロセッサと 16 Gb RAM を搭載した Windows 11 ノート PC の結果について説明します。

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

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

テスト ケース

.NET 8 で 2 つのシナリオをベンチマークします:

1. 繰り返し文字の数を使用して文字列を圧縮するシンプルな C# コード。たとえば、文字列「aabccccccaaa」は「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 操作によって、ベンチマーク結果が歪む可能性があります。

2 つの 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

2 番目のドキュメントはより小さくシンプルです。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>

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 の場合、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 AOT アプリケーションは、通常の .NET と比較して起動が高速です。公式ベンチマークでは、AOT アプリケーションのメモリ フットプリントが小さいことも示されています。

しかし、起動後は、管理対象アプリケーションの方が通常は速度が向上します。これは、JIT がランタイム情報にアクセスできるために起こります。実行時間の長いアプリケーションでは、動的プロファイル ガイドによる最適化やその他の手法に基づいて、より効果的なコードを再生成できます。

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