$30 off During Our Annual Pro Sale. View Details »

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

Avatar for neno neno
November 29, 2025

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

Avatar for neno

neno

November 29, 2025
Tweet

More Decks by neno

Other Decks in Technology

Transcript

  1. 自己紹介 1 • 所属: NTTドコモビジネス株式会社 イノベーションセンター • Microsoft MVP for

    Developer Technologies (2024~) • .NET / Web Development • 趣味: C#, OSS, ドール, 一眼(α7 IV), シーシャ • 執心領域 • C# ⇔ TypeScript • SignalR • Observability / OpenTelemetry 何縫ねの。 nenoNaninu nenoMake ブログ https://blog.neno.dev その他 https://neno.dev
  2. OSS 紹介 2 属性を付与するだけ Tapper • C# の型定義から TypeScript の型定義を生成する

    .NET Tool/ library • JSON / MessagePack 対応! https://github.com/nenoNaninu/Tapper
  3. OSS 紹介 3 • C# の SignalR Client を強く型付けするための Source

    Generator TypedSignalR.Client Before After (using TypedSignalR.Client) こんな SignalR の Hub と Receiver の interface が あったとして… 脱文字列! 全てが強く型付け! https://github.com/nenoNaninu/TypedSignalR.Client
  4. 4 • TypeScript の SignalR Client を強く型付けするための .NET Tool /

    library TypedSignalR.Client.TypeScript Before After (using TypedSignalR.Client.TypeScript) 脱文字列! 全てが強く型付け! TypeScript 用の型を C# から自動生成 MessagePack Hub Protocol 対応! https://github.com/nenoNaninu/TypedSignalR.Client.TypeScript 属性を付与するだけ! OSS 紹介
  5. 5 • SignalR 使ったアプリを快適に開発するための GUI を自動生成する library • 2 step

    で利用可能! • http pipeline に middleware の追加 • Hub と Receiver を定義してる interface に属性を付与 • JWT 認証 サポート • パラメータのユーザ定義型サポート • JSON で入力! SignalR 版 SwaggerUI TypedSignalR.Client.DevTools https://github.com/nenoNaninu/TypedSignalR.Client.DevTools OSS 紹介
  6. AspNetCore.SignalR.OpenTelemetry OSS 紹介 6 https://github.com/nenoNaninu/AspNetCore.SignalR.OpenTelemetry • トレースのための計装 • 最低限のログ •

    接続時 • Transport 層の情報も出力(WebSocket 等) • メソッド呼び出し時 • HubName.MethodName の素朴なログ • メソッド呼び出し毎にログのスコープを追加 • HubName, MethodName, InvocationId を 振っているのでログの検索性が向上 • Duration • 切断時 • 切断時に例外が発生していれば例外もログに出力 Inspired by HttpLogging SignalR のメソッド呼び出し毎に スパンが切られるように https://github.com/nenoNaninu/AspNetCore.SignalR.OpenTelemetry
  7. お品書き 10 • Object Stack Allocation • Devirtualization • Cloning

    • Inlining • Constant Folding • GC Write Barriers • VM • Threading • Reflection • Collections • LINQ
  8. Object Stack Allocation 12 Object stack allocation とは何か • 本来

    heap に allocation が発生してしまうところを stack に allocation するようにする最適化 • JIT は escape analysis を行い、オブジェクトがメソッドの スコープ外に出ていかない事を証明できた場合 heap ではなく stack に allocation する • .NET 9 でも object stack allocation が効く • https://speakerdeck.com/nenonaninu/dot-net-9-nopahuomansugai-shan?slide=25 • .NET 10 では最適化が効くパターンが拡大
  9. Object Stack Allocation 15 .NET 10 では delegate も object

    stack allocation の対象に この delegate は y をキャプチャしているので 毎回 heap に allocation がかかるハズ
  10. Object Stack Allocation 21 ベンチマークのコードの最終的な asm を .NET 9/10 で比較

    DisplayClass0 (24 byte) は 相変わらず new (CORINFO_HELP_NEWSFAST) しているが Func (64 byte) の new は消えている
  11. Object Stack Allocation 22 ベンチマークのコードの最終的な asm を .NET 9/10 で比較

    DisplayClass0 (24 byte) は 相変わらず new (CORINFO_HELP_NEWSFAST) しているが Func (64 byte) の new は消えている DoubleResult が インライン展開された
  12. Object Stack Allocation 25 .NET 10 では array も object

    stack allocation の対象に Array が stack allocation される
  13. Object Stack Allocation 26 .NET 10 では array も object

    stack allocation の対象に 昔から存在するコードではありがち Array が stack allocation される
  14. Object Stack Allocation 27 .NET 10 では array も object

    stack allocation の対象に 昔から存在するコードではありがち Array が stack allocation される 現代では ReadOnlySpan 使えば明示的に zero allocation が達成できる (ReadOnlySpan + collection 式 or params ReadOnlySpan)
  15. Object Stack Allocation 30 BitConverter.GetBytes でも最適化が効く 本来 BitConverter.GetBytes で 32

    byte (24 byte + 4 byte + padding) の allocation が発生してしまうハズだが .NET 10 ではその allocation が消失
  16. Object Stack Allocation 31 BitConverter.GetBytes でも最適化が効く 本来 BitConverter.GetBytes で 32

    byte (24 byte + 4 byte + padding) の allocation が発生してしまうハズだが .NET 10 ではその allocation が消失 64 bit 環境では配列の allocation に 最低でも 24 byte 必要 header (8byte) + type handle (8byte) + length (8byte)
  17. Object Stack Allocation 32 BitConverter.GetBytes でも最適化が効く 本来 BitConverter.GetBytes で 32

    byte (24 byte + 4 byte + padding) の allocation が発生してしまうハズだが .NET 10 ではその allocation が消失 type handle についてはこちら https://blog.neno.dev/entry/2025/11/09/214259 64 bit 環境では配列の allocation に 最低でも 24 byte 必要 header (8byte) + type handle (8byte) + length (8byte)
  18. Object Stack Allocation 33 BitConverter.GetBytes でも最適化が効く 本来 BitConverter.GetBytes で 32

    byte (24 byte + 4 byte + padding) の allocation が発生してしまうハズだが .NET 10 ではその allocation が消失 GetBytes と AsSpan がインライン展開される事で 本来 GetBytes で返される array が Copy3Bytes の スコープの中で完結するようになるので object stack allocation が可能 64 bit 環境では配列の allocation に 最低でも 24 byte 必要 header (8byte) + type handle (8byte) + length (8byte) type handle についてはこちら https://blog.neno.dev/entry/2025/11/09/214259
  19. • C# において配列は当然ながら非常に重要 • なのでガッツリ最適化されている • が、interface 経由の配列の呼び出しは最適化がイマイチだった • C#

    の配列には様々な interface (IList<T>, IReadOnlyList<T> 等) が実装されている • 配列に対する interface 実装は、配列以外の型に対する interface 実装とは 根本的に異なる実装方法が内部的には取られている • そのため JIT は配列以外の型に対して行える devirtualization が配列には適用できなかった Devirtualization 35 Array の devirtualization 事情
  20. Devirtualization 36 Array の devirtualization 事情 .NET 10 から interface

    経由の配列に対するメソッド呼び出しが ガッツリ最適化…! • C# において配列は当然ながら非常に重要 • なのでガッツリ最適化されている • が、interface 経由の配列の呼び出しは最適化がイマイチだった • C# の配列には様々な interface (IList<T>, IReadOnlyList<T> 等) が実装されている • 配列に対する interface 実装は、配列以外の型に対する interface 実装とは 根本的に異なる実装方法が内部的には取られている • そのため JIT は配列以外の型に対して行える devirtualization が配列には適用できなかった
  21. Devirtualization 38 どちらが高速だと思いますか?(.NET 9) Enumerable (GetEnumerator + MoveNext + Current)

    と インデックスアクセス (this[int]) なら インデックスアクセスの方が早いと考えるのが自然
  22. Devirtualization 39 どちらが高速だと思いますか?(.NET 9) Enumerable (GetEnumerator + MoveNext + Current)

    と インデックスアクセス (this[int]) なら インデックスアクセスの方が早いと考えるのが自然
  23. Devirtualization 40 どちらが高速だと思いますか?(.NET 9) だが実際には直感と異なる! (.NET 9 までは) Enumerable (GetEnumerator

    + MoveNext + Current) と インデックスアクセス (this[int]) なら インデックスアクセスの方が早いと考えるのが自然
  24. Devirtualization 41 どちらが高速だと思いますか?(.NET 9) Enumerable (GetEnumerator + MoveNext + Current)

    と インデックスアクセス (this[int]) なら インデックスアクセスの方が早いと考えるのが自然 だが実際には直感と異なる! (.NET 9 までは)
  25. Devirtualization 42 どちらが高速だと思いますか?(.NET 9) ToArray を ToList に変えると 直感と一致する (!!)

    Enumerable (GetEnumerator + MoveNext + Current) と インデックスアクセス (this[int]) なら インデックスアクセスの方が早いと考えるのが自然 だが実際には直感と異なる! (.NET 9 までは)
  26. Devirtualization 43 何故このような事態になるか? その1 • ReadOnlyCollection<T> は内部的に IList<T> オブジェクトを握っている •

    ReadOnlyCollection.GetEnumerator() は内部に抱えている IList<T> オブジェクトの GetEnumerator() を呼び出している • インデックスアクセスも同様に内部に抱えている IList<T> オブジェクトのインデクサを利用
  27. Devirtualization 44 何故このような事態になるか? その1 • ReadOnlyCollection<T> は内部的に IList<T> オブジェクトを握っている •

    ReadOnlyCollection.GetEnumerator() は内部に抱えている IList<T> オブジェクトの GetEnumerator() を呼び出している • インデックスアクセスも同様に内部に抱えている IList<T> オブジェクトのインデクサを利用 これらの virtual method をどこまで JIT が最適化できるか? というのがパフォーマンス的には焦点となる
  28. Devirtualization 45 何故このような事態になるか? その2 • .NET 9 では配列の interface 経由の呼び出しは

    devirtualization が困難 • GetEnumerator も this[int] も devirtualization されない • 一方で GetEnumerator で返される IEnumerator<T> は 普通の型なので devirtualization が効く • 故に interface 経由で毎回インデックスアクセスするより 一発 devirtualization されていない GetEnumerator を叩いて devirtualization された IEnumerator<T> で MoveNext / Current を叩く方が高速
  29. Devirtualization 47 .NET 10 では array に対する interface 経由のメソッド呼び出しが devirtualization

    されるように! JIT が array interface method に対して ガッツリ最適化された
  30. Devirtualization 48 .NET 10 では array に対する interface 経由のメソッド呼び出しが devirtualization

    されるように! .NET 10 では直感どおり インデックスアクセスの方が高速に! JIT が array interface method に対して ガッツリ最適化された
  31. Devirtualization 49 この最適化は LINQ にも間接的に良い影響を及ぼしている • LINQ では内部的に IList<T> は特別扱いされて最適化されている

    • 殆どのケースでこの最適化は有効に働く • しかし ReadOnlyCollection (IList<T> が実装されている) 等が用いられている 一部のケースでは、IList<T> に対する最適化が逆効果であった
  32. Devirtualization 50 この最適化は LINQ にも間接的に良い影響を及ぼしている • LINQ では内部的に IList<T> は特別扱いされて最適化されている

    • 殆どのケースでこの最適化は有効に働く • しかし ReadOnlyCollection (IList<T> が実装されている) 等が用いられている 一部のケースでは、IList<T> に対する最適化が逆効果であった .NET 10 では 期待通り IList<T> に対する 最適化が有効に働く
  33. Devirtualization 51 Guarded Devirtualization (GDV) もより賢く • Generic context 下において

    virtual call は GDV で最適化されないケースが存在した • .NET 10 からは generic context 下における virtual call でも GDV が働くように!
  34. Cloning 61 昔から存在する最適化テクニック 一番大きい index に対して 一番最初にアクセスする とはいえ、JIT 的には この手の最適化は素朴にはできない

    (挙動が変わってしまうため) この例だと範囲チェックが一度だけ実行され 0~6 に対する範囲チェックが消し飛ぶ
  35. Cloning 62 昔から存在する最適化テクニック 一番大きい index に対して 一番最初にアクセスする とはいえ、JIT 的には この手の最適化は素朴にはできない

    (挙動が変わってしまうため) そこで cloning による最適化を行う この例だと範囲チェックが一度だけ実行され 0~6 に対する範囲チェックが消し飛ぶ
  36. Cloning 67 .NET 10 ではどうなるか? 隣の asm は C# 的にはこういう事

    JIT 時に Cloning による最適化 上のブロックは 範囲チェックが外れる
  37. Cloning 68 .NET 10 ではどうなるか? 隣の asm は C# 的にはこういう事

    JIT 時に Cloning による最適化 上のブロックは 範囲チェックが外れる 下のブロックは 従来通りの範囲チェック
  38. Cloning 70 .NET 10 では Span に対しても cloning の最適化適用されるように count

    は span/arr と無関係なので 範囲チェックが必要
  39. Cloning 71 .NET 10 では Span に対しても cloning の最適化適用されるように count

    は span/arr と無関係なので 範囲チェックが必要
  40. Cloning 72 .NET 10 では Span に対しても cloning の最適化適用されるように count

    は span/arr と無関係なので 範囲チェックが必要 Span かつ .NET 9 の場合 毎回範囲チェック
  41. Cloning 73 .NET 10 では Span に対しても cloning の最適化適用されるように count

    は span/arr と無関係なので 範囲チェックが必要 Span かつ .NET 9 の場合 毎回範囲チェック Array の場合は Cloning で範囲チェックが 外れた asm が生成
  42. Cloning 74 .NET 10 では Span に対しても cloning の最適化適用されるように count

    は span/arr と無関係なので 範囲チェックが必要 Span かつ .NET 9 の場合 毎回範囲チェック Array の場合は Cloning で範囲チェックが 外れた asm が生成
  43. Cloning 75 .NET 10 では Span に対しても cloning の最適化適用されるように count

    は span/arr と無関係なので 範囲チェックが必要 Span かつ .NET 9 の場合 毎回範囲チェック .NET 10 からは Span に対しても cloning が適用され 条件次第で範囲チェックが外れる Array の場合は Cloning で範囲チェックが 外れた asm が生成
  44. Inlining 82 .NET 10 からは try/finally を含むメソッドでもインライン展開 .NET 9 では

    普通に method を call している .NET 10 では try/finally の インライン展開が有効に
  45. Inlining 83 .NET 10 からは try/finally を含むメソッドでもインライン展開 try/catch のインライン展開は まだ課題がある模様

    .NET 9 では 普通に method を call している .NET 10 では try/finally の インライン展開が有効に
  46. Inlining 84 Generic Virtual Method (GVM) についてもインライン展開 • .NET 9

    まで GVM はインライン展開の対象外だった • .NET 10 からは GVM に対してもインライン展開が有効に
  47. Constant Folding 90 Constant Folding とは Constant Folding らしい Constant

    Folding runtime 的には定数 JIT 時に計算して 定数ベタ書き
  48. Constant Folding 93 Constant Folding は定数の演算だけではない runtime は GetString が

    null を返さない事を知っている 例外の throw はデッドコードなので JIT 時に削除
  49. Constant Folding 94 Constant Folding は定数の演算だけではない runtime は GetString が

    null を返さない事を知っている 例外の throw はデッドコードなので JIT 時に削除 これもまた Constant Folding
  50. Constant Folding 97 .NET 10 では null check に関する Constant

    Folding が強化 null check が 2回発生している
  51. Constant Folding 98 .NET 10 では null check に関する Constant

    Folding が強化 null check が 2回発生している null check が1回に!
  52. Constant Folding 99 .NET 10 では null check に関する Constant

    Folding が強化 null check が 2回発生している null check が1回に! AggressiveInlining
  53. • gen0 のオブジェクトの大半の参照は gen0 のオブジェクトが握っているハズという仮定を置いている • gen0 のオブジェクトを GC で回収する場合

    gen0 のみをスキャンすれば高速に回収できるハズ GC Write Barriers 101 .NET の GC は世代別 GC ヒューリスティックな最適化が行われている
  54. • gen0 のオブジェクトの大半の参照は gen0 のオブジェクトが握っているハズという仮定を置いている • gen0 のオブジェクトを GC で回収する場合

    gen0 のみをスキャンすれば高速に回収できるハズ • ただし gen0 のオブジェクトへの参照を gen1, gen2 に存在するオブジェクトが握っていた場合 gen0 のみをスキャンしてオブジェクトを回収してしまうと大問題 GC Write Barriers 102 .NET の GC は世代別 GC ヒューリスティックな最適化が行われている
  55. • JIT と GC は連携して古い世代から新しい世代への参照を追跡する • e.g., gen1 から gen0

    への参照を追跡する • 世代間を跨ぐ可能性のある参照書き込みが発生する度に card table と内部的に呼ばれるものに 追跡のための情報を書き込む関数が呼び出される • そのような asm を JIT は出力する • この機構は GC write barrier と呼称される GC Write Barriers 103 .NET の GC はこの問題を解決するためにどうしているか?
  56. • 何故なら参照書き込みの度に GC write barrier は発生するため • .NET 9 時点で

    GC write barrier のための関数は複数あり JIT が最適なものを選択して asm を出力している GC Write Barriers 104 GC write barrier は高速でなければいけない
  57. • A: そもそも GC write barrier が発生しない事 GC Write Barriers

    107 Q: 最も高速な GC write barrier は? .NET 10 では GC write barrier を省く最適化が強化!
  58. • A: そもそも GC write barrier が発生しない事 GC Write Barriers

    108 Q: 最も高速な GC write barrier は? .NET 10 では GC write barrier を省く最適化が強化! .NET 9 時点でも GC write barrier が 不要な場合は省く最適化は実装されている
  59. • ref struct は heap に絶対に存在する事ができない型 • Heap に絶対に存在できないという事は前述してきた GC

    write barrier が必要だった理由が根本的に成立しない • オブジェクトが heap に存在できないという事は、 そもそも GC の世代を跨ぐ可能性が絶対にない • なので GC write barrier は不要! • ちなみに ref struct の事を runtime 内部では byref-like types と呼称する GC Write Barriers 109 .NET 10 では ref struct に対する GC write barrier が最適化
  60. GC Write Barriers 111 .NET 10 では ref struct に対する

    GC write barrier が最適化 .NET 9 では GC write barrier が 発生している
  61. GC Write Barriers 112 .NET 10 では ref struct に対する

    GC write barrier が最適化 .NET 9 では GC write barrier が 発生している .NET 10 では GC write barrier が 発生していない…!
  62. • int, float, pointer, object reference 等の サイズが小さい値を返す場合は CPU で予約されている

    レジスタを用いれば良い • レジスタに収まらないサイズが大きい値型は? • 呼び出し元が return buffer を確保して、 その return buffer に対する pointer を暗黙的な引数としてメソッドに渡して 返り値を書き込ませる GC Write Barriers 113 メソッドの返り値をどうやって呼び出し元に返すか?
  63. • .NET 9 • return buffer は基本的に呼び出し元の stack frame 内に確保される

    • ただしあくまで基本的にでしかなかった • 仕様的には stack ではなく heap に確保することも可能であった • .NET 10 • 絶対に呼び出し元の stack frame 内に確保されるように仕様変更 GC Write Barriers 114 .NET 10 で return buffer に対する仕様変更が発生
  64. • GC write barrier が不要になる! • 呼び出されたメソッドは return buffer に値を書き込む際、

    stack に書き込んでいるか heap に書き込んでいるかわからない • そのため必ず GC write barrier を挟む必要があった • 絶対 stack に確保されるなら、GC write barrier を挟む必要がない! • Return buffer に書き込まれた値型の値を参照型のフィールドに 格納したりするのは呼び出し元のお仕事 • その際には GC write barrier が発生する GC Write Barriers 115 Return buffer が stack に絶対確保される制約の何が嬉しいか?
  65. GC Write Barriers 118 Return buffer に対する仕様変更によるパフォーマンス改善 .NET 9 では

    GC write barrier が発生している (CORINFO_HELP_CHECKED_ASSIGN_REF)
  66. GC Write Barriers 119 Return buffer に対する仕様変更によるパフォーマンス改善 .NET 9 では

    GC write barrier が発生している (CORINFO_HELP_CHECKED_ASSIGN_REF)
  67. GC Write Barriers 120 Return buffer に対する仕様変更によるパフォーマンス改善 .NET 9 では

    GC write barrier が発生している (CORINFO_HELP_CHECKED_ASSIGN_REF) .NET 10 では GC write barrier が 発生していない…!
  68. • Unboxing はもともと C で実装されていたが、C# に移植 • Native code と

    managed code の切り替えのオーバヘッドが無くなる • 全て managed code であれば、JIT が最適化する余地が生まれるので パフォーマンスが向上する VM 122 Runtime 内部で C で記述されていたコードを C# (System.Private.CoreLib) に移植
  69. VM 123 Runtime 内部で C で記述されていたコードを C# (System.Private.CoreLib) に移植 .NET

    9 と .NET 10 の JIT が生成する asm の大きな違いが出る
  70. Threading 125 前提知識: ThreadPool の queue と work item について

    • ThreadPool には複数の queue が含まれる • Global queue: ThreadPool に 1 つ • Local queue: ThreadPool に含まれる thread 毎に 1 つ • ThreadPool に work item を投げる場合 • ThreadPool 外の thread からなら global queue に • ThreadPool 内の thread からなら基本的には local queue に https://blog.neno.dev/entry/2023/05/27/152855
  71. Threading 126 前提知識: ThreadPool の queue と work item について

    • ThreadPool には複数の queue が含まれる • Global queue: ThreadPool に 1 つ • Local queue: ThreadPool に含まれる thread 毎に 1 つ • ThreadPool に work item を投げる場合 • ThreadPool 外の thread からなら global queue に • ThreadPool 内の thread からなら基本的には local queue に .NET Core 2.1 で追加された ThreadPool.QueueUserWorkItem, ThreadPool.UnsafeQueueUserWorkItem では preferLocal で queue を選択可能 https://blog.neno.dev/entry/2023/05/27/152855
  72. Threading 127 前提知識: ThreadPool の queue と work item について

    • ThreadPool には複数の queue が含まれる • Global queue: ThreadPool に 1 つ • Local queue: ThreadPool に含まれる thread 毎に 1 つ • ThreadPool に work item を投げる場合 • ThreadPool 外の thread からなら global queue に • ThreadPool 内の thread からなら基本的には local queue に .NET Core 2.1 で追加された ThreadPool.QueueUserWorkItem, ThreadPool.UnsafeQueueUserWorkItem では preferLocal で queue を選択可能 https://blog.neno.dev/entry/2023/05/27/152855 UnsafeQueueUserWorkItem の Unsafe って何?と思った方はこちら
  73. Threading 128 前提知識: local queue が空の場合の挙動 • 別の queue に処理するべき

    work item が無いか探して実行する ① まず Global queue から work item を取得しようとする ② Global queue が空の場合、 他の local queue から work item を取得して 他の thread を支援しようとする
  74. Threading 129 前提知識: local queue が空の場合の挙動 • 別の queue に処理するべき

    work item が無いか探して実行する ① まず Global queue から work item を取得しようとする ② Global queue が空の場合、 他の local queue から work item を取得して 他の thread を支援しようとする work stealing と呼ばれる
  75. Threading 130 前提知識: 前述の ThreadPool の挙動の設計意図は? • Global queue への競合を最小限に抑える

    • 既に処理されている work item と 論理的に関連ある work item の処理を優先 • 分かりやすくは await 後の continuation は 優先的に処理されてほしいですよね?的なモチベーション
  76. Threading 131 前提知識: 前述の ThreadPool の挙動の設計意図は? • Global queue への競合を最小限に抑える

    • 既に処理されている work item と 論理的に関連ある work item の処理を優先 • 分かりやすくは await 後の continuation は 優先的に処理されてほしいですよね?的なモチベーション 非常に効率よく機能している
  77. Threading 132 ただし問題がないわけではなかった (.NET 9 時点) • thread を block

    するような、 ベストプラクティスに反している処理を行っている場合は 問題が起きる事がある • 特に sync over async で問題が発生しやすい
  78. Threading 133 Sync over async で発生する問題 • Sync over async

    では thread が block される • Task.Result とか Task.GetAwaiter().GetResult() とか使うと block される • 本当は適切なタイミングで continuation が実行されてほしい • continuation は local queue に積まれる • しかし thread が sync over async により block されている場合、 何時まで経っても local queue に積まれている work item は実行されない • 結果的に別の thread の local queue が空になり、 work stealing が発生し、block されている thread の local queue から work item が steal されるまで continuation が実行されない
  79. Threading 134 Sync over async で発生する問題 つまり global queue に

    work item が 「常に」積まれている場合 何時までたっても continuation は実行されない!!
  80. Threading 135 Sync over async で発生する問題 つまり global queue に

    work item が 「常に」積まれている場合 何時までたっても continuation は実行されない!! .NET 10 ではこの問題が解決!!
  81. Threading 136 .NET 10 ではどうなったか? • スレッドがブロック状態になるとき、 local queue に積まれている

    work item を global queue に積みなおすようになった • これにより今まで block された thread にとって最優先だった work item は 他の thread にとって最も低い優先度なものとして扱われていたが .NET 10 からは block された thread の work item が 他の thread から公平に処理される機会を得られるようになった • Q: 何故他の thread にとって最も低い優先度であったか? • A: global queue が空にならないと処理されない work item であったため
  82. Threading 137 .NET 10 ではどうなったか? • スレッドがブロック状態になるとき、 local queue に積まれている

    work item を global queue に積みなおすようになった • これにより今まで block された thread にとって最優先だった work item は 他の thread にとって最も低い優先度なものとして扱われていたが .NET 10 からは block された thread の work item が 他の thread から公平に処理される機会を得られるようになった • Q: 何故他の thread にとって最も低い優先度であったか? • A: global queue が空にならないと処理されない work item であったため
  83. Reflection 139 .NET 8 で UnsafeAccessorAttribute が導入 • Reflection なしに

    public でないメンバに無理やりアクセスする仕組み • だたし制約があった
  84. Reflection 144 UnsafeAccessorAttribute の制約 その3 • 別 assembly の private

    type が method の parameter に使われている場合無力
  85. Reflection 145 UnsafeAccessorAttribute の制約 その3 • 別 assembly の private

    type が method の parameter に使われている場合無力
  86. Reflection 149 .NET 10 で UnsafeAccessorTypeAttribute の導入 Assembly A Assembly

    B 別 Assembly の private type に対して [UnsafeAccessorType(“type名, assembly 名”)] を 用いることでアクセス可能に
  87. Reflection 150 UnsafeAccessorTypeAttribute は BCL 内部で活用されている • System.Net.Http は System.Security.Cryptography

    に依存している • そのため System.Security.Cryptography からはSystem.Net.Http に依存できない • しかし System.Security.Cryptography は HTTP request で OCSP 情報を取ってくるため System.Net.Http を参照したい • .NET 9 までは reflection で System.Net.Http を参照していた • .NET 10 からは UnsafeAccessorAttribute, UnsafeAccessorTypeAttribute を を用いることで reflection なしで System.Net.Http を参照可能に • Reflection が消えたので高速化!
  88. • たとえば List<T> を具象型のまま foreach するのは効率的 • GetEnumerator() は構造体を返すため効率的 •

    一方で List<T> を IEnumerable<T> として扱うと foreach した時 IEnumerator<T> を介してしまうので非効率 • boxing による allocation の発生 • interface dispatch の発生 Collections 152 Enumeration の効率化を図りたい
  89. • たとえば List<T> を具象型のまま foreach するのは効率的 • GetEnumerator() は構造体を返すため効率的 •

    一方で List<T> を IEnumerable<T> として扱うと foreach した時 IEnumerator<T> を介してしまうので非効率 • boxing による allocation の発生 • interface dispatch の発生 Collections 153 Enumeration の効率化を図りたい これらは構造体を直接扱っている場合は発生しない課題
  90. • JIT は特定のメソッドで最も頻繁に扱われる具象型を特定する • 特定した具象型に対して特化したコードを生成する • Devirtualization / Inlining /

    Object stack allocation • .NET 10 では T[], List<T> に対してより積極的に最適化が行われる • さらに T[], List<T> 以外でもより最適化が効くよう .NET 10 から IEnumerator に対して [Intrinsic] が付与された Collections 154 Dynamic PGO による Enumeration の最適化 https://speakerdeck.com/nenonaninu/dot-net-8-deji-ding-deyou-xiao-ninatuta-dynamic-pgo-nituite
  91. • JIT は特定のメソッドで最も頻繁に扱われる具象型を特定する • 特定した具象型に対して特化したコードを生成する • Devirtualization / Inlining /

    Object stack allocation • .NET 10 では T[], List<T> に対してより積極的に最適化が行われる • さらに T[], List<T> 以外でもより最適化が効くよう .NET 10 から IEnumerator に対して [Intrinsic] が付与された Collections 155 Dynamic PGO による Enumeration の最適化 Dynamic PGO についてはこちら https://speakerdeck.com/nenonaninu/dot-net-8-deji-ding-deyou-xiao-ninatuta-dynamic-pgo-nituite
  92. Collections 160 なぜコレクションの要素数が増加すると allocation が発生してしまうのか? コレクションの要素数が多いと MoveNextRare は MoveNext が呼び出される回数に

    対して相対的に呼び出される回数が減り その結果インライン展開されなくなり enumerator が stack allocation されなくなる
  93. Collections 161 なぜコレクションの要素数が増加すると allocation が発生してしまうのか? コレクションの要素数が多いと MoveNextRare は MoveNext が呼び出される回数に

    対して相対的に呼び出される回数が減り その結果インライン展開されなくなり enumerator が stack allocation されなくなる 昔は MoveNext をインライン展開するため このような実装が適切な最適化だったが 昨今の Dynamic PGO 等の最適化が進む中で 適切な最適化ではなくなってしまった
  94. Collections 162 なぜコレクションの要素数が増加すると allocation が発生してしまうのか? コレクションの要素数が多いと MoveNextRare は MoveNext が呼び出される回数に

    対して相対的に呼び出される回数が減り その結果インライン展開されなくなり enumerator が stack allocation されなくなる .NET 10 では現代に即した 最適化が効きやすい形に再実装 昔は MoveNext をインライン展開するため このような実装が適切な最適化だったが 昨今の Dynamic PGO 等の最適化が進む中で 適切な最適化ではなくなってしまった
  95. Collections 168 MoveNext を再実装した結果 https://speakerdeck.com/nenonaninu/dot-net-8-deji-ding-deyou-xiao-ninatuta-dynamic-pgo-nituite?slide=19 しかしそれでも要素数が増えると Zero allocation ではなくなってしまう なぜか?

    これは on-stack replacement (OSR) の影響 OSR で最適化されたコードには PGO 用の計測コードが含まれていない 結果的にループの終端に対する Dynamic PGO のために使う計測情報が取れない (e.g., enumerator の Dispose を呼び出す際等の計測情報が取れない) なぜなら終端が Tire0 で呼び出されるより先に OSR で最適化されたコードが用いられるようになるため
  96. Collections 169 MoveNext を再実装した結果 https://speakerdeck.com/nenonaninu/dot-net-8-deji-ding-deyou-xiao-ninatuta-dynamic-pgo-nituite?slide=19 しかしそれでも要素数が増えると Zero allocation ではなくなってしまう なぜか?

    これは on-stack replacement (OSR) の影響 OSR で最適化されたコードには PGO 用の計測コードが含まれていない 結果的にループの終端に対する Dynamic PGO のために使う計測情報が取れない (e.g., enumerator の Dispose を呼び出す際等の計測情報が取れない) なぜなら終端が Tire0 で呼び出されるより先に OSR で最適化されたコードが用いられるようになるため そのため GDV が働かず List<T>.Enumerator.Dispose ではなく IEnumerator<T>.Dispose を呼び出してしまい allocation が発生してしまう
  97. Collections 170 MoveNext を再実装した結果 https://speakerdeck.com/nenonaninu/dot-net-8-deji-ding-deyou-xiao-ninatuta-dynamic-pgo-nituite?slide=19 しかしそれでも要素数が増えると Zero allocation ではなくなってしまう なぜか?

    これは on-stack replacement (OSR) の影響 OSR で最適化されたコードには PGO 用の計測コードが含まれていない 結果的にループの終端に対する Dynamic PGO のために使う計測情報が取れない (e.g., enumerator の Dispose を呼び出す際等の計測情報が取れない) なぜなら終端が Tire0 で呼び出されるより先に OSR で最適化されたコードが用いられるようになるため そのため GDV が働かず List<T>.Enumerator.Dispose ではなく IEnumerator<T>.Dispose を呼び出してしまい allocation が発生してしまう OSR についてはこちら
  98. Collections 173 .NET 10 では JIT の最適化により OSR で発生する課題に対処 Dynamic

    PGO 的に欠けている計測情報を 周辺の enumerator の使われ方に対する 計測情報で補って最適化するようになった https://github.com/dotnet/runtime/pull/118461 Zero allocation
  99. Collections: Stack 174 .NET 9 時点の Stack の enumeration の実装

    • .NET 9 時点では Stack の enumeration には多くの分岐が潜んでいた • 1. version の確認 • GetEnumerator したタイミングから stack に変更がないか • 2. enumerator に対する最初の呼び出しかの判定 • 3. enumeration が終了しているかの判定 • 4. 終わっていないなら次に列挙する要素が残っているか確認 • 5. 内部に抱えている配列から要素を取得 • 当然ながら範囲チェックが発生する
  100. Collections: Stack 175 .NET 10 での Stack の enumeration に対する最適化

    • .NET 10 ではコードが半分になった • enumerator の初期化時にコンストラクタで stack の index を取得 • MoveNext 時には index をデクリメントすればいいだけ • 列挙し終えたら index は負になる • つまり、継続するべきかどうかは以下の 1 行でチェック可能 • 要素を読み取るまでに発生する分岐は2つだけになった • Version の確認 • Index の範囲チェック • 結果的にコード量 / メンバのサイズ / 分岐数が減り 分岐予測 / インライン展開 / stack allocation 等の最適化が効きやすい
  101. Collections: Stack 176 .NET 10 での Stack の enumeration に対する最適化

    • .NET 10 ではコードが半分になった • enumerator の初期化時にコンストラクタで stack の index を取得 • MoveNext 時には index をデクリメントすればいいだけ • 列挙し終えたら index は負になる • つまり、継続するべきかどうかは以下の 1 行でチェック可能 • 要素を読み取るまでに発生する分岐は2つだけになった • Version の確認 • Index の範囲チェック • 結果的にコード量 / メンバのサイズ / 分岐数が減り 分岐予測 / インライン展開 / stack allocation 等の最適化が効きやすい イディオムとして覚えておくと良さそう
  102. Collections: Stack 178 Stack の enumerator の実装を .NET 9/10 で比較

    .NET 9 .NET 10 .NET 9 では index は -2 → (Count -1) → … → -1
  103. Collections: Stack 179 Stack の enumerator の実装を .NET 9/10 で比較

    .NET 9 .NET 10 .NET 10 では index は Count → … → -1 .NET 9 では index は -2 → (Count -1) → … → -1
  104. Collections: Stack 180 Stack の enumerator の実装を .NET 9/10 で比較

    .NET 9 .NET 10 .NET 10 では コード量、分岐量ともに激減 .NET 10 では index は Count → … → -1 .NET 9 では index は -2 → (Count -1) → … → -1
  105. Collections: Queue 182 Queue の enumeration に対しても Stack 同様の最適化を実施 •

    Queue は Stack と違い、内部の配列に対する index が循環する • index が ^1 の時、次の index が 0 になる場合がある • これに対しては index % array.Length で対処可能 • 実際 .NET Framework では % array.Length が使われていた • だが、当然ながら除算は重たい処理なので、避けたい
  106. Collections: Queue 183 .NET 9 時点での Queue の enumeration •

    除算は避けたいので、.NET 9 では以下のように実装されていた • しかし、これでも甘い。分岐が2つ発生してしまっている • 配列の長さに対するチェック • 配列にインデックスアクセス時の範囲チェック
  107. Collections: Queue 185 Queue の enumerator の実装を .NET 9/10 で比較

    .NET 9 .NET 10 .NET 9/10 ともに index (_index/_i) は -1 → … → Count → -2
  108. Collections: Queue 186 Queue の enumerator の実装を .NET 9/10 で比較

    .NET 9 .NET 10 .NET 9/10 ともに index (_index/_i) は -1 → … → Count → -2
  109. Collections: Queue 187 Queue の enumerator の実装を .NET 9/10 で比較

    .NET 9 .NET 10 .NET 9/10 ともに index (_index/_i) は -1 → … → Count → -2 重要なのはココ
  110. Collections: Queue 189 .NET 10 における Queue の MoveNext の最適化

    index が配列の長さ未満の場合 これは範囲チェックの分岐が吹き飛ぶので 分岐が1回で済む .NET 10 .NET 9
  111. Collections: Queue 190 .NET 10 における Queue の MoveNext の最適化

    index が配列の長さ未満の場合 これは範囲チェックの分岐が吹き飛ぶので 分岐が1回で済む index が配列の長さを超えている場合 こちらは範囲チェックの分岐挟まるので 分岐が2回発生する .NET 10 .NET 9
  112. Collections: ConcurrentDictionary 192 ConcurrentDictionary の構造 • ConcurrentDictionary はバケットのコレクションになっている • 要素はバケットに保存されている

    • バケットのコレクションは linked list として実装されている • そのため総舐めする際には二重のループが必要になっている • バケットのコレクションのループ • バケットの中身のループ
  113. Collections: ConcurrentDictionary 195 .NET 9 時点での ConcurrentDictionary.Enumerator の MoveNext は複雑

    Irreducible なループが発生してしまっている 下の例のように A, B どちらからでもループを開始できる場合は 取り扱いが難しく最適化が困難 (irreducible)
  114. Collections: ConcurrentDictionary 196 .NET 9 時点での ConcurrentDictionary.Enumerator の MoveNext は複雑

    Irreducible なループが発生してしまっている 一方で A, B どちらかからループが始まる事が 証明できるようなループは最適化できる (reducible) 下の例のように A, B どちらからでもループを開始できる場合は 取り扱いが難しく最適化が困難 (irreducible)
  115. LINQ 200 .NET 9 でも LINQ の劇的な改善が行われた • .NET 9

    では IIListProvider<T> / IPartition<T> が排除された • 全て Iterator<T> ベースの実装に刷新された • IIListProvider<T> / IPartition<T> の責務が Iterator<T> に集約された • Iterator<T> 自体は .NET 8 以前から存在する class • .NET 9 のタイミングで Iterator<T> は高速化のために拡張された • interface dispatch ではなく virtual dispatch になった https://github.com/dotnet/runtime/pull/98969
  116. LINQ 203 なぜ Iterator<T> のような最適化を行うか? OrderBy した後 First を叩く場合 ソートする必要はなく

    純粋に key が最小値の値を探索すればいいだけ Iterator<T> 等を用いることで 上流のオペレータは後続のオペレータに対して適切な処理を提供可能 (e.g., OrderBy の後続に First が続くなら実際にはソートはしない) .NET 8 までは同様の最適化を IIListProvider<T> / IPartition<T> で行っていた
  117. LINQ 204 なぜ Iterator<T> のような最適化を行うか? OrderBy した後 First を叩く場合 ソートする必要はなく

    純粋に key が最小値の値を探索すればいいだけ Iterator<T> 等を用いることで 上流のオペレータは後続のオペレータに対して適切な処理を提供可能 (e.g., OrderBy の後続に First が続くなら実際にはソートはしない) .NET 8 までは同様の最適化を IIListProvider<T> / IPartition<T> で行っていた OrderBy は内部的に OrderedIterator<TElement, TKey> を返す
  118. LINQ 205 なぜ Iterator<T> のような最適化を行うか? OrderBy した後 First を叩く場合 ソートする必要はなく

    純粋に key が最小値の値を探索すればいいだけ Iterator<T> 等を用いることで 上流のオペレータは後続のオペレータに対して適切な処理を提供可能 (e.g., OrderBy の後続に First が続くなら実際にはソートはしない) .NET 8 までは同様の最適化を IIListProvider<T> / IPartition<T> で行っていた OrderBy は内部的に OrderedIterator<TElement, TKey> を返す
  119. LINQ 206 なぜ Iterator<T> のような最適化を行うか? OrderBy した後 First を叩く場合 ソートする必要はなく

    純粋に key が最小値の値を探索すればいいだけ Iterator<T> 等を用いることで 上流のオペレータは後続のオペレータに対して適切な処理を提供可能 (e.g., OrderBy の後続に First が続くなら実際にはソートはしない) .NET 8 までは同様の最適化を IIListProvider<T> / IPartition<T> で行っていた OrderBy は内部的に OrderedIterator<TElement, TKey> を返す First 内部ではこの TryGetFirst を用いる
  120. LINQ 207 .NET 10 では特に Contains が強化された • Contains が

    OrderBy, Distinct, Reverse 等のオペレータの 後続に続く場合、上流のオペレータの処理は無視できる
  121. LINQ 209 Shuffle().Take(n) の最適化 • Shuffle は .NET 10 で追加されたオペレータ

    • Shuffle の後続に Take が続く場合、実際に Shuffle する必要はない! • どうすればいいか? • Source から n 個の要素を一様にサンプリングできればいいだけ! • サンプリングには Reservoir sampling を用いる
  122. LINQ 210 Shuffle().Take(n) の最適化 • Shuffle は .NET 10 で追加されたオペレータ

    • Shuffle の後続に Take が続く場合、実際に Shuffle する必要はない! • どうすればいいか? • Source から n 個の要素を一様にサンプリングできればいいだけ! • サンプリングには Reservoir sampling を用いる Reservoir sampling については過去の資料で解説しています https://speakerdeck.com/nenonaninu/dot-net-8-deji-ding-deyou-xiao-ninatuta-dynamic-pgo-nituite?slide=50
  123. LINQ 212 Shuffle().Take(n).Contains() の最適化 • Shuffle().Take(n).Contains() をこの LINQ は totalCount

    個の要素の内 Contains で指定している対象と一致する要素が equalCount 個存在し totalCount 個の内から takeCount 個サンプリングした時 比較対象と一致するものは含まれているか? という確率問題に帰着する • ちなみに超幾何分布と呼ばれる確率分布の確立問題
  124. LINQ 213 Shuffle().Take(n).Contains() の最適化問題の具体的な問題 • 100 個の要素があり、目的の対象となる要素が 20 個含まれている •

    100 個の内から 5 個ピックアップした時、 目的の対象が 1 個以上含まれている確率は? 1 − 80 100 × 79 99 × 78 98 × 77 97 × 76 96 1個も目的の要素が得られない確率を求めて1から引けばいい (義務教育レベル!)
  125. LINQ 217 UseSizeOptimizedLinq オプションの導入 • もともと System.Linq.dll のビルドには2パターン存在した • CoreCLR

    向け • パフォーマンス優先 • アセンブリサイズは犠牲 • NativeAOT 向け • アプリケーションのアセンブリサイズ優先 • パフォーマンスは犠牲 .NET 10 からは パフォーマンスの高い LINQ を NativeAOT でも利用できる