Upgrade to Pro — share decks privately, control downloads, hide ads and more …

.NET 9 のパフォーマンス改善

neno
December 21, 2024

.NET 9 のパフォーマンス改善

.NET Conf / .NET ラボ 2024/12/21 での発表資料

関連資料
C# 13 / .NET 9 の新機能 (RC 1 時点)
.NET 8 で既定で有効になった Dynamic PGO について

neno

December 21, 2024
Tweet

More Decks by neno

Other Decks in Technology

Transcript

  1. 何縫ねの。 NTT コミュニケーションズ イノベーションセンター Microsoft MVP for Developer Technologies (2024~)

    .NET / Web Development nenoNaninu @nenoMake ブログ https://blog.neno.dev その他 https://neno.dev
  2. OSS SignalR 周りのいろいろ開発しています •TypedSignalR.Client • C# の SignalR Client を強く型付けするための

    Source Generator •TypedSignalR.Client.TypeScript • TypeScript の SignalR Client を強く型付けするための .NET Tool / library •TypedSignalR.Client.DevTools • SignalR 向けに Swagger UI 相当の GUI を自動生成する library •AspNetCore.SignalR.OpenTelemetry • SignalR の OpenTelemetry 対応 + log 強化 https://github.com/nenoNaninu
  3. Agenda Dynamic PGO Tier 0 Optimization Object Stack Allocation GC

    VM Threading Reflection SearchValues<string> Span
  4. Dynamic PGO •.NET 8 まで • Devirtualization • virtual method,

    interface method, delegate の呼び出し高速化 • Inlining •.NET 9 から • Cast の高速化 • (T)obj • obj is T
  5. Dynamic PGO .NET 8 必ず IsInstanceOfClass を呼び出して obj が B

    かを判定 B の TypeHandle と _obj が渡される
  6. Dynamic PGO .NET 8 必ず IsInstanceOfClass を呼び出して obj が B

    かを判定 B の TypeHandle と _obj が渡される 継承等の関係上単純な比較にはならない
  7. Dynamic PGO IsInstanceOfClass の実装詳細 或いはそもそも TypeHandle とは何か https://learn.microsoft.com/en-us/archive/msdn-magazine/2005/may/net-framework-internals-how-the-clr-creates-runtime-objects • Type

    毎に MethodTalbe が1つ存在する • TypeHandle は MethodTalbe へのポインタ • MethodTable には Type に関する様々な情報 • 型の種別(class, struct, interface) • 実装している interface の数 • 等々
  8. Dynamic PGO .NET 9 重要なのはここ。 JIT は C が B

    から継承されている事を知っている。 なので obj が C かを直接判定(cmp)し高速化
  9. Dynamic PGO .NET 9 C であれば下にそのまま処理が進み C ではなかったら M00_L01 に飛ぶ

    重要なのはここ。 JIT は C が B から継承されている事を知っている。 なので obj が C かを直接判定(cmp)し高速化
  10. Tier 0 Optimization でも generics なら? まぁ、あるよね。 .NET 8 までは、Tire

    0 では最適化されず boxing が発生 Tire 1 で最適化され、呼び出し自体が消し飛ぶ
  11. Tier 0 Optimization でも generics なら? まぁ、あるよね。 .NET 8 までは、Tire

    0 では最適化されず boxing が発生 Tire 1 で最適化され、呼び出し自体が消し飛ぶ .NET 9 からは Tire 0 で呼び出しが消し飛ぶ
  12. Object Stack Allocation heap に allocation が発生する所を stack に allocation

    する最適化 普通に考えれば new MyObj() で heap に allocation が走る
  13. Object Stack Allocation .NET 9 Object stack allocation + inlining

    により、定数を返すだけになる Heap に対する allocation なし
  14. Object Stack Allocation .NET 9 Object stack allocation + inlining

    により、定数を返すだけになる Heap に対する allocation なし 流石にパフォーマンス アピール用のコード 過ぎないか?
  15. Object Stack Allocation .NET 9 Object stack allocation + inlining

    により、定数を返すだけになる Heap に対する allocation なし 流石にパフォーマンス アピール用のコード 過ぎないか? 実際のユースケースでは どうだろうか?
  16. .NET 8 こういうユースケースで嬉しい Object Stack Allocation MyStruct.Dispose() の実装が 空なので inlining

    され ここまで圧縮される using で dispose する場合は この展開のされ方になるが boxing は発生しない
  17. .NET 8 こういうユースケースで嬉しい Object Stack Allocation MyStruct.Dispose() の実装が 空なので inlining

    され ここまで圧縮される using で dispose する場合は この展開のされ方になるが boxing は発生しない .NET 9
  18. .NET 8 こういうユースケースで嬉しい .NET 9 Object Stack Allocation disposable はこのフレーム内で

    参照が握られたりしない事が証明できるため、 object stack allocation による最適化の対象となる using で dispose する場合は この展開のされ方になるが boxing は発生しない MyStruct.Dispose() の実装が 空なので inlining され ここまで圧縮される
  19. .NET 8 こういうユースケースで嬉しい .NET 9 Object Stack Allocation disposable はこのフレーム内で

    参照が握られたりしない事が証明できるため、 object stack allocation による最適化の対象となる using で dispose する場合は この展開のされ方になるが boxing は発生しない MyStruct.Dispose() の実装が 空なので inlining され ここまで圧縮される
  20. GC • メモリの消費量を抑える • 一回あたりの GC の時間を抑える • タイムアウト有り •

    GUI をフリーズさせないため • CPU のコア数が 1 つなら必ず Workstation GC • ヒープは 1 つ Server GC • スループットの最大化 • メモリの消費量は大きめ • 一回あたりの GC の時間長め • タイムアウト無し • ASP.NET Core の既定 • ただし CPU コアが2つ以上の場合 • CPUコア数=ヒープ数 • ただし既定の場合 Workstation GC GC の種別
  21. GC AdaptationMode • スループットは大事だけど、メモリの消費量も抑えたい • DATAS (Dynamically Adapting To Application

    Sizes) の導入 • .NET 8 で導入された、しかし既定で有効にはなっていなかった • .NET 9 から既定で有効に • DATAS のお仕事は「アプリケーションのサイズに適応する」事 • ヒープ数の調整 • スループットと消費するメモリと GC の頻度に応じて調整 • 完全な compaction を走らせるための allocation budget の調整 https://maoni0.medium.com/dynamically-adapting-to-application-sizes-2d72fcb6f1ea
  22. GC AdaptationMode • スループットは大事だけど、メモリの消費量も抑えたい • DATAS (Dynamically Adapting To Application

    Sizes) の導入 • .NET 8 で導入された、しかし既定で有効にはなっていなかった • .NET 9 から既定で有効に • DATAS のお仕事は「アプリケーションのサイズに適応する」事 • ヒープ数の調整 • スループットと消費するメモリと GC の頻度に応じて調整 • 完全な compaction を走らせるための allocation budget の調整 https://maoni0.medium.com/dynamically-adapting-to-application-sizes-2d72fcb6f1ea 完全な compaction を走らせないと 何時まで経っても余計なメモリを食いっぱなし
  23. GC AdaptationMode • スループットは大事だけど、メモリの消費量も抑えたい • DATAS (Dynamically Adapting To Application

    Sizes) の導入 • .NET 8 で導入された、しかし既定で有効にはなっていなかった • .NET 9 から既定で有効に • DATAS のお仕事は「アプリケーションのサイズに適応する」事 • ヒープ数の調整 • スループットと消費するメモリと GC の頻度に応じて調整 • 完全な compaction を走らせるための allocation budget の調整 https://maoni0.medium.com/dynamically-adapting-to-application-sizes-2d72fcb6f1ea メモリの消費量気にせず 最大のスループットを求めるなら 今のところ AdaptationMode は切った方が良い 完全な compaction を走らせないと 何時まで経っても余計なメモリを食いっぱなし
  24. VM FCALL から QCALL へ • managed code から runtime

    code の呼び出し手段 • QCALL • 実質 runtime で定義された関数への P/Invoke • GC を止めない • FCALL • より特殊で複雑なメカニズムを持つ呼び出し手段 • QCALL に比較すると重い • GC を止める • 以前は FCALL が主流だったが、リリース毎に徐々に QCALL に置き換えられている
  25. VM 例外の高速化 • Native AOT の例外ハンドリングの実装を coreclr に移植 • 例外処理が

    3.5 ~ 4 倍高速化(自称) • 実際には 2~4倍くらい? • global spinlock の排除
  26. Threading System.Threading.Lock • .NET 8 までは object で lock するのが常套手段

    • .NET 9 からは System.Threading.Lock の利用を推奨
  27. Threading System.Threading.Interlocked • Exchange / CompareExchange に overload が追加 •

    byte • sbyte • ushort • short Parallel.ForAsync<T> (.NET 8~) 内部でも 利用され、パフォーマンス改善
  28. Threading System.Threading.Interlocked • Exchange / CompareExchange に overload が追加 •

    byte • sbyte • ushort • short • Exchange<T> / CompareExchange <T> の class 制約の排除 • generics で primitive type が使えるようになったため、使い勝手向上 • enum に対応 Parallel.ForAsync<T> (.NET 8~) 内部でも 利用され、パフォーマンス改善
  29. Threading System.Threading.Interlocked • Exchange / CompareExchange に overload が追加 •

    byte • sbyte • ushort • short • Exchange<T> / CompareExchange <T> の class 制約の排除 • generics で primitive type が使えるようになったため、使い勝手向上 • enum に対応 Parallel.ForAsync<T> (.NET 8~) 内部でも 利用され、パフォーマンス改善 CompareExchange の都合上 int (4 byte) にしていたところを .NET 9 からは bool (1 byte) に置き換えられるため、オブジェクトのサイズを落とせる
  30. Reflection ConstructorInvoker (.NET 8) • ConstructorInvoker 自体は .NET 8 で導入された型

    • .NET 9 から Ms.Ex.DependencyInjection 内部で利用されるようになった 同種の MethodInvoker も .NET 8 で導入
  31. SearchValues<string> .NET 9 から SearchValues<T> が string で使えるように…! • .NET

    8 で導入された SearchValues<T> • .NET 8 時点では byte と char のみの対応だった
  32. Span ReadOnlySpan<T> を引数に持つメソッドへの params アノテーション祭り • C# 13 の新機能である params

    collection にあらゆるところで対応 • 再コンパイルするだけで高速化の可能性…! • params T[] が呼ばれていた箇所が params ReadOnlySpan<T> が利用されるようになるため • 実際に params が追加された API 群 (一部) • StringBuilder • Path / StreamWriter / TextWriter • Task / CancellationTokenSource • ImmutableArray / ImmutableHashSet / ImmutableList / ImmutableQueue / ImmutableStack • Counter / UpDownCounter / Histogram / Measurement / TagList
  33. Span ReadOnlySpan の初期化の改善 • .NET 8 時点で byte / sbyte

    / bool の定数の ReadOnlySpan<T> を作成する場合は高速 • アセンブリのデータを ReadOnlySpan で読むだけなので非常に高速
  34. Span ReadOnlySpan の初期化の改善 • .NET 8 時点で byte / sbyte

    / bool の定数の ReadOnlySpan<T> を作成する場合は高速 • アセンブリのデータを ReadOnlySpan で読むだけなので非常に高速 コレクションリテラル導入以前から 存在する最適化
  35. Span ReadOnlySpan の初期化の改善 エンディアンの問題で 1 byte の primitive type に限られていた

    コレクションリテラル導入以前から 存在する最適化 • .NET 8 時点で byte / sbyte / bool の定数の ReadOnlySpan<T> を作成する場合は高速 • アセンブリのデータを ReadOnlySpan で読むだけなので非常に高速
  36. Span ReadOnlySpan の初期化の改善 • .NET 9 ではすべての primitive type に同種の最適化が適用され高速に

    • RuntimeHelpers.CreateSpan を用いる事で解決される運びとなった https://github.com/dotnet/runtime/issues/60948