.NET 11ランタイムの新機能
What's new in the .NET 11 runtime (learn.microsoft.com)
要約
.NET 11ランタイムでは、ハードウェア要件の更新、ランタイムネイティブな非同期処理(Runtime Async V2)の導入、JITコンパイラの最適化、ハードウェアイントリンジックの拡充など、多くの改善が行われています。これらの変更は、パフォーマンスの向上、デバッグ体験の改善、およびより広範なハードウェアサポートを目指しています。
全文翻訳
.NET 11ランタイムの新機能
この記事では、.NET 11ランタイムの新機能について説明します。最終更新はリリース候補2(RC 2)向けです。
更新された最小ハードウェア要件
.NET 11の最小ハードウェア要件は、x86/x64およびArm64アーキテクチャの両方で、よりモダンな命令セットを要求するように更新されました。さらに、ReadyToRun(R2R)コンパイルターゲットも、新しいハードウェア機能を活用できるように更新されています。
Arm64要件
Appleの場合、最小ハードウェアまたはReadyToRunターゲットに変更はありません。Apple M1チップは約armv8.5-aに相当し、少なくともAdvSimd(NEON)、CRC、DOTPROD、LSE、RCPC、RCPC2、RDMA命令セットをサポートしています。
Linuxの場合、最小ハードウェアに変更はありません。.NETは、AdvSimd命令セットのみをサポートする可能性のあるRaspberry Piなどのデバイスを引き続きサポートします。ReadyToRunターゲットはLSE命令セットを含むように更新され、アプリケーションを起動する際にジッティング(JITコンパイル)のオーバーヘッドが増加する可能性があります。
Windowsの場合、ベースラインはLSE命令セットを要求するように更新されました。これはWindows 11で要求されており、Windows 10で公式にサポートされているすべてのArm64 CPUによっても要求されています。さらに、Arm SBSA(Server Base System Architecture)の要件とも一致しています。ReadyToRunターゲットはarmv8.2-a + RCPCに更新され、少なくともAdvSimd、CRC、LSE、RCPC、RDMAをサポートし、公式にサポートされているハードウェアの大部分をカバーします。
| OS | 以前のJIT/AOT最小要件 | 新しいJIT/AOT最小要件 | 以前のR2Rターゲット | 新しいR2Rターゲット |
|---|---|---|---|---|
| Apple | Apple M1 | (変更なし) | Apple M1 | (変更なし) |
| Linux | armv8.0-a | (変更なし) | armv8.0-a | armv8.0-a + LSE |
| Windows | armv8.0-a | armv8.0-a + LSE | armv8.0-a | armv8.2-a + RCPC |
x86/x64要件
すべての3つのオペレーティングシステム(Apple、Linux、Windows)で、ベースラインがx86-64-v1からx86-64-v2に更新されました。これにより、ハードウェアはCMOV、CX8、SSE、SSE2のみを保証するのではなく、CX16、POPCNT、SSE3、SSSE3、SSE4.1、SSE4.2も保証するようになります。この保証はWindows 11およびWindows 10で公式にサポートされているすべてのx86/x64 CPUで必要とされます。これには、IntelおよびAMDによって公式にサポートされているすべてのチップが含まれており、古いチップのサポートは2013年頃に終了しています。
ReadyToRunターゲットは、WindowsおよびLinuxではx86-64-v3に更新され、さらにAVX、AVX2、BMI1、BMI2、F16C、FMA、LZCNT、MOVBE命令セットが含まれるようになりました。AppleのReadyToRunターゲットは変更されません。
| OS | 以前のJIT/AOT最小要件 | 新しいJIT/AOT最小要件 | 以前のR2Rターゲット | 新しいR2Rターゲット |
|---|---|---|---|---|
| Apple | x86-64-v1 | x86-64-v2 | x86-64-v2 | (変更なし) |
| Linux | x86-64-v1 | x86-64-v2 | x86-64-v2 | x86-64-v3 |
| Windows | x86-64-v1 | x86-64-v2 | x86-64-v2 | x86-64-v3 |
影響
.NET 11以降、.NETは古いハードウェアで実行できなくなり、次のようなメッセージが表示される可能性があります。
現在のCPUは、ベースライン命令セットの1つ以上が不足しています。
ReadyToRun対応のアセンブリでは、典型的なデバイスで期待されるサポートを満たさない一部のハードウェアで、起動時のオーバーヘッドが増加する可能性があります。
変更理由
.NETは、多くの場合、基盤となるオペレーティングシステムの最小ハードウェア要件を上回る、幅広いハードウェアをサポートしています。このサポートは、特にまだ使用されていない可能性が高い古いハードウェアにとって、コードベースにかなりの複雑さを加えています。さらに、AOTターゲットがデフォルトで従うべき「最低共通 denominador」を定義しており、シナリオによってはパフォーマンスの低下につながる可能性があります。
最小ベースラインの更新は、コードベースの保守の複雑さを軽減し、基盤となるオペレーティングシステムの文書化された(そしてしばしば強制される)ハードウェア要件とよりよく連携するために行われました。
詳細については、最小ハードウェア要件の更新を参照してください。
Runtime Async
.NET 11では、ランタイムネイティブな非同期処理(Runtime Async V2)が導入されました。これは、コンパイラ生成の非同期ステートマシンをランタイム管理のサスペンションと再開に置き換えるための重要なステップです。コンパイラがステートマシンクラスを生成する代わりに、ランタイム自体が非同期実行を追跡し、よりクリーンなスタックトレース、デバッグ性の向上、およびオーバーヘッドの削減を実現します。
Runtime Asyncはプレビュー機能です。オプトインするには、プロジェクトファイルに次のプロパティを追加します。
<PropertyGroup>
<Features>runtime-async=on</Features>
</PropertyGroup>
net11.0プロジェクトでは、Runtime Asyncを使用するために<EnablePreviewFeatures>true</EnablePreviewFeatures>は不要になりました。
.NETランタイムライブラリ自体はruntime-async=onでコンパイルされています。ランタイムライブラリはコンパイラ生成のステートマシンを含まなくなり、完全にランタイム提供の非同期に依存するようになりました。これにより、アプリ全体(ライブラリ依存関係のみ)を新しいモデルに移行することが可能になり、機能とパフォーマンスの広範な検証が提供されます。製品チームは、観測されるスループットとライブラリサイズの変更について、肯定的または否定的な報告を歓迎します。
.NET 11には、さらに2つの改善が含まれています。
- 共変
Task→Task<T>オーバーライド: 派生クラスが、Taskを返す基底メソッドに対してTask<T>を返す場合、ランタイムは呼び出し規約の違いを橋渡しするvoidを返すシンクを生成するため、仮想ディスパッチが両方の種類で機能します。同じ修正がNativeAOTにも適用されます。 - crossgen2でのインライン化: ReadyToRun(R2R)コンパイル中にランタイム非同期メソッドのインライン化を妨げていた制限が解除されました。すべての非同期テストはcrossgen2とcomposite R2Rの両方でパスし、awaitのない非同期呼び出し(同期高速パス)のインライン化がエンドツーエンドで確認されています。
注意
以前はランタイム非同期の動作を制御していたDOTNET_RuntimeAsyncおよびUNSUPPORTED_RuntimeAsync環境変数は削除されました。プロジェクトごとにランタイム非同期をオプトアウトするには、環境変数に依存する代わりに、プロジェクトファイルで<UseRuntimeAsync>false</UseRuntimeAsync>を設定してください。
よりクリーンなライブスタックトレース
最も目に見える改善は、ライブスタックトレース、つまりプロファイラ、デバッガ、およびnew StackTrace()が実行中に見るものです。コンパイラ生成の非同期では、各非同期メソッドはステートマシンインフラストラクチャから複数のフレームを生成します。Runtime Asyncでは、実際のメソッドが呼び出しスタックに直接表示されます。
// ランタイム非同期を有効にするには、.csprojに以下を追加します。
// <Features>runtime-async=on</Features>
await OuterAsync();
static async Task OuterAsync()
{
await Task.CompletedTask;
await MiddleAsync();
}
static async Task MiddleAsync()
{
await Task.CompletedTask;
await InnerAsync();
}
static async Task InnerAsync()
{
await Task.CompletedTask;
Console.WriteLine(new StackTrace(fNeedFileInfo: true));
}
runtime-asyncなし — 13フレーム、ステートマシンインフラストラクチャが表示される:
at Program.<<Main>$>g__InnerAsync|0_2() in Program.cs:line 24
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.<<Main>$>g__InnerAsync|0_2()
at Program.<<Main>$>g__MiddleAsync|0_1() in Program.cs:line 14
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.<<Main>$>g__MiddleAsync|0_1()
at Program.<<Main>$>g__OuterAsync|0_0() in Program.cs:line 8
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.<<Main>$>g__OuterAsync|0_0()
at Program.<Main>$ (String[] args) in Program.cs:line 3
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
at Program.<Main>$ (String[] args)
at Program.<Main>(String[] args)
runtime-asyncあり — 5フレーム、実際の呼び出しチェーン:
at Program.<<Main>$>g__InnerAsync|0_2() in Program.cs:line 24
at Program.<<Main>$>g__MiddleAsync|0_1() in Program.cs:line 14
at Program.<<Main>$>g__OuterAsync|0_0() in Program.cs:line 8
at Program.<Main>$ (String[] args) in Program.cs:line 3
at Program.<Main>(String[] args)
注意
例外スタックトレース(catch (Exception ex)から)は、Runtime Asyncの有無にかかわらず既に同じように見えます。これは、コンパイラ生成コードの既存のExceptionDispatchInfoクリーンアップがそのケースを処理するためです。改善は、ライブ実行中に表示されるものにあります。
この改善は、プロファイリングツール、診断ログ、デバッガの呼び出しスタックウィンドウなど、ライブ実行スタックを検査するあらゆるものに役立ちます。
NativeAOTおよびReadyToRunサポート
Runtime Asyncは、NativeAOTおよびReadyToRunコンパイルをサポートしています。これにより、機能はJITコンパイルコードを超えて、Ahead-of-Timeコンパイルシナリオにも拡張されます。ランタイムは継続オブジェクトをより積極的に再利用し、変更されていないローカル変数の保存を回避して、非同期処理が多いコードでの割り当て圧力を削減します。
デバッグの改善
ブレークポイントがランタイム非同期メソッド内で正しくバインドされるようになり、デバッガはコンパイラ生成インフラストラクチャにジャンプすることなく、await境界をステップ実行できるようになりました。
Runtime Asyncのパフォーマンス改善
Runtime Asyncには、いくつかのパフォーマンス改善が含まれています。
- JIT非同期サポート: JITは、シンクを介して委譲するのではなく、同期的なタスクを返すメソッドの専用ランタイム非同期バージョンをコンパイルします。JITはメソッドのテール呼び出しをランタイム非同期呼び出しに変換し、そうでなければ返されるタスクを待機することで、追加の参照レイヤーを排除します。
- テールマージされたサスペンションポイント: JITは非同期サスペンションポイントをテールマージするため、生成されるコードが小さくなります。
- キャッシュされた継続: ランタイム非同期呼び出し可能なタスクシンクに使用される継続はキャッシュされ、再利用されます。
- プールされたメソッドはオプトアウト: 既にプールされているメソッドはランタイム非同期からオプトアウトし、冗長な作業を回避します。
- 階層化された非同期コンパイル: 非同期バージョンは階層化されたコンパイルを通過するようになったため、ホットな非同期メソッドはウォームアップ後にティア1最適化を受けます。
- TaskおよびValueTaskファクトリイントリンジック: JITは、
Task.FromResult、Task.CompletedTask、ValueTask.FromResultなどの一般的なファクトリを認識し、それらをより高速な非同期パスに折り込みます。 - テールawait最適化: メソッドが別の非同期結果を直接返す場合、非同期メソッドからの暗黙的なテール呼び出しが再び有効になり、
await Task.Yield()はランタイム非同期パスでの追加の割り当てを回避します。
ExecutionContextなしの非同期継続
非同期継続は、ExecutionContextのキャプチャと復元をオプトアウトできるようになりました。ExecutionContextは、awaitポイントを横断して(AsyncLocal<T>値などの)アンビエント状態を伝達します。すべてのTask継続は以前はコンテキストのスナップショットをキャプチャし、実行前にそれを復元していましたが、AsyncLocal<T>状態が使用されておらず、復元がノープ(no-op)であった場合でも同様でした。
ランタイムは現在、継続が復元するものがないことを検出すると、キャプチャ/復元サイクルを完全にスキップします。Task、Task<T>、ValueTask、ValueTask<T>はすべてこの変更から恩恵を受け、ランタイム非同期実装パスも同様です。ConfigureAwait(false)およびAsyncLocal<T>をあまり使用しないアプリケーションでは、高スループットの非同期コードパスでのオーバーヘッドが削減されます。
JITの改善
- 境界チェックの削除: ジャストインタイム(JIT)コンパイラは、インデックスに定数を加えたものが長さを超えるかどうかの比較(例:
i + cns < len)や、末尾からのインデックスアクセス(例:values[^1])の一般的なパターンで、境界チェックを削除するようになりました。これにより、タイトなループでの冗長なチェックが削減され、配列およびスパン操作のスループットが向上します。 - 冗長なチェックコンテキストの削除: JITは、値が既に範囲内にあることがわかっている場合など、冗長なチェック算術コンテキストを証明して削除できるようになりました。この最適化により、生成コードでの不要なオーバーフローチェックが排除されます。
- スイッチ式折りたたみ: ターゲットが少数の定数(例:
x is 0 or 1 or 2 or 3 or 4)である場合、マルチターゲットswitch式は、より単純な分岐なしのチェックに折りたたまれるようになりました。 - 高速なuintからfloat/doubleへのキャスト:
uintからfloatまたはdoubleへのキャストは、AVX-512以前のx86ハードウェアでより高速になりました。 - SVE2イントリンジック: 新しいArm SVE2(Scalable Vector Extension 2)イントリンジックが利用可能になりました:
ShiftRightLogicalNarrowingSaturate(Even|Odd)。これらは、SVE2をサポートするArmハードウェアで利用可能なベクトル化操作のセットを拡張します。 - x64での
Math.BigMul:Math.BigMul(long, long, out long)は、x64で大幅に高速化されました。JITは、両方のオペランドが64ビット値であり、呼び出し元が結果の高位半分を要求する場合に単一のMUL r/m64命令を生成し、以前のヘルパー呼び出しを排除します。 - 単一IGプロローグ制限の解除: JITは、関数プロローグが単一の命令グループ(IG)に収まることを要求しなくなりました。多くのレジスタの保存、大きなスタック割り当て、またはランタイム非同期状態の設定を伴う複雑なプロローグは、フォールバックパスをトリガーしなくなりました。
SELECT(cond, cns, cns)折りたたみ: JITは、2つのブランチが両方とも同じ定数を生成する条件付きセレクトを、その定数に折りたたむようになりました(例:condition ? 42 : 42は42になります)。この折りたたみは、以前の最適化で分岐が統合された後に現れる可能性のある不要な比較を排除します。- AVX-VNNI-512イントリンジック: System.Runtime.Intrinsics.X86.AvxVnni.V512は、
AVX512-VNNIサポートを持つCPUで、AVX-VNNI乗算加算イントリンジックの512ビット形式を公開します。 - 飽和浮動小数点変換: チェックされていない
floatおよびdoubleから小さな整数型への変換は、中間切り捨てによるラップではなく、型境界に飽和するようになりました。 - ARM64でのSVE参照による
Vector<T>: ランタイムがARM SVEサポートでコンパイルされている場合、Vector<T>値は値ではなく参照で渡されます。これは、スケーラブルな型のARM呼び出し規約と一致し、SVEを多用するコードのコード生成を改善します。
パフォーマンスとコード品質の向上を目指し、.NET 11ではさらにいくつかのJIT最適化が追加されています。
定数折りたたみシーケンス比較
JITは、オペランドが両方ともコンパイル時定数であるstring.EqualsまたはReadOnlySpan<T>.SequenceEqual呼び出しを折りたたむことができるようになりました。これにより、バイトごとの比較が定数trueまたはfalseの結果に置き換えられます。これは、インライン化の後、呼び出し元が既知の文字列と比較するヘルパーに別のリテラルを渡す場合に最も重要になります。IsAdminが`