このページには自動翻訳されたテキストを含めることができます。
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 環境もあります。
アプリケーションは、さまざまな構成でもテストされます。ある構成のアプリケーションが 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 ベンチマークを使うと、パフォーマンスの観点からさまざまな構成を比較できます。ただし、結果はオペレーティング システムとプロセッサ アーキテクチャに依存します。最適なデプロイメント構成を見つけるには、対象環境で独自のベンチマークを実行する必要があります。