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

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

Avatar for neno neno
December 21, 2024

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

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

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

Avatar for neno

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