このページには自動翻訳されたテキストを含めることができます。
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 環境もあります。
アプリケーションはさまざまな構成でもテストされます。一部の構成のアプリケーションは「シナリオ」を定義します。
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 ベンチマークを使用すると、パフォーマンスの観点からさまざまな構成を比較できます。ただし、結果はオペレーティング システムとプロセッサ アーキテクチャによって異なります。最適な展開構成を見つけるには、ターゲット環境で独自のベンチマークを実行する必要があります。