Slide 1

Slide 1 text

何縫ねの。 .NET 10 のパフォーマンス改善 CLR/H #111 .NET Conf 2025 Tokyo 2025/11/29 2025/12/20

Slide 2

Slide 2 text

自己紹介 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

Slide 3

Slide 3 text

OSS 紹介 2 属性を付与するだけ Tapper • C# の型定義から TypeScript の型定義を生成する .NET Tool/ library • JSON / MessagePack 対応! https://github.com/nenoNaninu/Tapper

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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 紹介

Slide 6

Slide 6 text

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 紹介

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

OSS 紹介 7 https://github.com/nenoNaninu/AspireExtensions • Aspire の便利な hosting integration • gRPC UI • Minio AspireExtensions

Slide 9

Slide 9 text

関連資料 8 https://speakerdeck.com/nenonaninu/net-10-noxin-ji-neng-rc-1-shi-dian C# 14 と .NET 10 の新機能はこの資料読んでおけばバッチリ!

Slide 10

Slide 10 text

関連資料 9 https://speakerdeck.com/nenonaninu/dot-net-9-nopahuomansugai-shan .NET 9 のパフォーマンス改善についてはこちらを是非!

Slide 11

Slide 11 text

お品書き 10 • Object Stack Allocation • Devirtualization • Cloning • Inlining • Constant Folding • GC Write Barriers • VM • Threading • Reflection • Collections • LINQ

Slide 12

Slide 12 text

Object Stack Allocation 11

Slide 13

Slide 13 text

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 では最適化が効くパターンが拡大

Slide 14

Slide 14 text

Object Stack Allocation 13 .NET 10 では delegate も object stack allocation の対象に

Slide 15

Slide 15 text

Object Stack Allocation 14 .NET 10 では delegate も object stack allocation の対象に

Slide 16

Slide 16 text

Object Stack Allocation 15 .NET 10 では delegate も object stack allocation の対象に この delegate は y をキャプチャしているので 毎回 heap に allocation がかかるハズ

Slide 17

Slide 17 text

Object Stack Allocation 16 ベンチマークコードを展開すると以下のようになる

Slide 18

Slide 18 text

Object Stack Allocation 17 ベンチマークコードを展開すると以下のようになる .NET 10 では delegate の allocation が 消失することで allocation が減少する

Slide 19

Slide 19 text

Object Stack Allocation 18 ベンチマークのコードの最終的な asm を .NET 9/10 で比較

Slide 20

Slide 20 text

Object Stack Allocation 19 ベンチマークのコードの最終的な asm を .NET 9/10 で比較

Slide 21

Slide 21 text

Object Stack Allocation 20 ベンチマークのコードの最終的な asm を .NET 9/10 で比較

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Object Stack Allocation 23 .NET 10 では array も object stack allocation の対象に

Slide 25

Slide 25 text

Object Stack Allocation 24 .NET 10 では array も object stack allocation の対象に

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Object Stack Allocation 27 .NET 10 では array も object stack allocation の対象に 昔から存在するコードではありがち Array が stack allocation される 現代では ReadOnlySpan 使えば明示的に zero allocation が達成できる (ReadOnlySpan + collection 式 or params ReadOnlySpan)

Slide 29

Slide 29 text

Object Stack Allocation 28 BitConverter.GetBytes でも最適化が効く

Slide 30

Slide 30 text

Object Stack Allocation 29 BitConverter.GetBytes でも最適化が効く

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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)

Slide 33

Slide 33 text

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)

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Devirtualization 34

Slide 36

Slide 36 text

• C# において配列は当然ながら非常に重要 • なのでガッツリ最適化されている • が、interface 経由の配列の呼び出しは最適化がイマイチだった • C# の配列には様々な interface (IList, IReadOnlyList 等) が実装されている • 配列に対する interface 実装は、配列以外の型に対する interface 実装とは 根本的に異なる実装方法が内部的には取られている • そのため JIT は配列以外の型に対して行える devirtualization が配列には適用できなかった Devirtualization 35 Array の devirtualization 事情

Slide 37

Slide 37 text

Devirtualization 36 Array の devirtualization 事情 .NET 10 から interface 経由の配列に対するメソッド呼び出しが ガッツリ最適化…! • C# において配列は当然ながら非常に重要 • なのでガッツリ最適化されている • が、interface 経由の配列の呼び出しは最適化がイマイチだった • C# の配列には様々な interface (IList, IReadOnlyList 等) が実装されている • 配列に対する interface 実装は、配列以外の型に対する interface 実装とは 根本的に異なる実装方法が内部的には取られている • そのため JIT は配列以外の型に対して行える devirtualization が配列には適用できなかった

Slide 38

Slide 38 text

Devirtualization 37 どちらが高速だと思いますか?(.NET 9)

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Devirtualization 45 何故このような事態になるか? その2 • .NET 9 では配列の interface 経由の呼び出しは devirtualization が困難 • GetEnumerator も this[int] も devirtualization されない • 一方で GetEnumerator で返される IEnumerator は 普通の型なので devirtualization が効く • 故に interface 経由で毎回インデックスアクセスするより 一発 devirtualization されていない GetEnumerator を叩いて devirtualization された IEnumerator で MoveNext / Current を叩く方が高速

Slide 47

Slide 47 text

Devirtualization 46 .NET 10 では array に対する interface 経由のメソッド呼び出しが devirtualization されるように!

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Devirtualization 48 .NET 10 では array に対する interface 経由のメソッド呼び出しが devirtualization されるように! .NET 10 では直感どおり インデックスアクセスの方が高速に! JIT が array interface method に対して ガッツリ最適化された

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Devirtualization 51 Guarded Devirtualization (GDV) もより賢く • Generic context 下において virtual call は GDV で最適化されないケースが存在した • .NET 10 からは generic context 下における virtual call でも GDV が働くように!

Slide 53

Slide 53 text

Cloning 52

Slide 54

Slide 54 text

Cloning 53 配列の範囲チェック問題

Slide 55

Slide 55 text

Cloning 54 配列の範囲チェック問題

Slide 56

Slide 56 text

Cloning 55 配列の範囲チェック問題 .NET 9 時点では インデックスアクセス毎に 範囲チェックが挟まっていた

Slide 57

Slide 57 text

Cloning 56 配列の範囲チェック問題 .NET 9 時点では インデックスアクセス毎に 範囲チェックが挟まっていた パフォーマンス的に よろしくない

Slide 58

Slide 58 text

Cloning 57 配列の範囲チェック問題 .NET 9 時点では インデックスアクセス毎に 範囲チェックが挟まっていた パフォーマンス的に よろしくない 範囲チェックは 1 度だけ 実行されるようにしたい

Slide 59

Slide 59 text

Cloning 58 昔から存在する最適化テクニック

Slide 60

Slide 60 text

Cloning 59 昔から存在する最適化テクニック 一番大きい index に対して 一番最初にアクセスする

Slide 61

Slide 61 text

Cloning 60 昔から存在する最適化テクニック 一番大きい index に対して 一番最初にアクセスする この例だと範囲チェックが一度だけ実行され 0~6 に対する範囲チェックが消し飛ぶ

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Cloning 63 .NET 10 ではどうなるか?

Slide 65

Slide 65 text

Cloning 64 .NET 10 ではどうなるか? JIT 時に Cloning による最適化

Slide 66

Slide 66 text

Cloning 65 .NET 10 ではどうなるか? JIT 時に Cloning による最適化

Slide 67

Slide 67 text

Cloning 66 .NET 10 ではどうなるか? 隣の asm は C# 的にはこういう事 JIT 時に Cloning による最適化

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Cloning 69 .NET 10 では Span に対しても cloning の最適化適用されるように

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Inlining (インライン展開) 76

Slide 78

Slide 78 text

• インライン展開はメソッド呼び出しのコスト削減に目が行きがち • 実際にはインライン展開の最も大きな利点は 連鎖的に最適化が効くこと Inlining 77 インライン展開のメリットは メソッドの呼び出しコスト削減だけではない インライン展開 インライン展開の結果 さらなる最適化が可能に

Slide 79

Slide 79 text

Inlining 78 .NET 10 からは try/finally を含むメソッドでもインライン展開

Slide 80

Slide 80 text

Inlining 79 .NET 10 からは try/finally を含むメソッドでもインライン展開

Slide 81

Slide 81 text

Inlining 80 .NET 10 からは try/finally を含むメソッドでもインライン展開 .NET 9 では 普通に method を call している

Slide 82

Slide 82 text

Inlining 81 .NET 10 からは try/finally を含むメソッドでもインライン展開 .NET 9 では 普通に method を call している

Slide 83

Slide 83 text

Inlining 82 .NET 10 からは try/finally を含むメソッドでもインライン展開 .NET 9 では 普通に method を call している .NET 10 では try/finally の インライン展開が有効に

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

Inlining 84 Generic Virtual Method (GVM) についてもインライン展開 • .NET 9 まで GVM はインライン展開の対象外だった • .NET 10 からは GVM に対してもインライン展開が有効に

Slide 86

Slide 86 text

Constant Folding (定数畳み込み) 85

Slide 87

Slide 87 text

Constant Folding 86 Constant Folding とは

Slide 88

Slide 88 text

Constant Folding 87 Constant Folding とは runtime 的には定数

Slide 89

Slide 89 text

Constant Folding 88 Constant Folding とは runtime 的には定数

Slide 90

Slide 90 text

Constant Folding 89 Constant Folding とは runtime 的には定数 JIT 時に計算して 定数ベタ書き

Slide 91

Slide 91 text

Constant Folding 90 Constant Folding とは Constant Folding らしい Constant Folding runtime 的には定数 JIT 時に計算して 定数ベタ書き

Slide 92

Slide 92 text

Constant Folding 91 Constant Folding は定数の演算だけではない

Slide 93

Slide 93 text

Constant Folding 92 Constant Folding は定数の演算だけではない runtime は GetString が null を返さない事を知っている

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

Constant Folding 95 .NET 10 では null check に関する Constant Folding が強化

Slide 97

Slide 97 text

Constant Folding 96 .NET 10 では null check に関する Constant Folding が強化

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

GC Write Barriers 100

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

• 何故なら参照書き込みの度に GC write barrier は発生するため • .NET 9 時点で GC write barrier のための関数は複数あり JIT が最適なものを選択して asm を出力している GC Write Barriers 104 GC write barrier は高速でなければいけない

Slide 106

Slide 106 text

GC Write Barriers 105 Q: 最も高速な GC write barrier は?

Slide 107

Slide 107 text

• A: そもそも GC write barrier が発生しない事 GC Write Barriers 106 Q: 最も高速な GC write barrier は?

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

• A: そもそも GC write barrier が発生しない事 GC Write Barriers 108 Q: 最も高速な GC write barrier は? .NET 10 では GC write barrier を省く最適化が強化! .NET 9 時点でも GC write barrier が 不要な場合は省く最適化は実装されている

Slide 110

Slide 110 text

• 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 が最適化

Slide 111

Slide 111 text

GC Write Barriers 110 .NET 10 では ref struct に対する GC write barrier が最適化

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

• int, float, pointer, object reference 等の サイズが小さい値を返す場合は CPU で予約されている レジスタを用いれば良い • レジスタに収まらないサイズが大きい値型は? • 呼び出し元が return buffer を確保して、 その return buffer に対する pointer を暗黙的な引数としてメソッドに渡して 返り値を書き込ませる GC Write Barriers 113 メソッドの返り値をどうやって呼び出し元に返すか?

Slide 115

Slide 115 text

• .NET 9 • return buffer は基本的に呼び出し元の stack frame 内に確保される • ただしあくまで基本的にでしかなかった • 仕様的には stack ではなく heap に確保することも可能であった • .NET 10 • 絶対に呼び出し元の stack frame 内に確保されるように仕様変更 GC Write Barriers 114 .NET 10 で return buffer に対する仕様変更が発生

Slide 116

Slide 116 text

• 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 に絶対確保される制約の何が嬉しいか?

Slide 117

Slide 117 text

GC Write Barriers 116 Return buffer に対する仕様変更によるパフォーマンス改善

Slide 118

Slide 118 text

GC Write Barriers 117 Return buffer に対する仕様変更によるパフォーマンス改善

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

VM 121

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

VM 123 Runtime 内部で C で記述されていたコードを C# (System.Private.CoreLib) に移植 .NET 9 と .NET 10 の JIT が生成する asm の大きな違いが出る

Slide 125

Slide 125 text

Threading 124

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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 って何?と思った方はこちら

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

Threading 132 ただし問題がないわけではなかった (.NET 9 時点) • thread を block するような、 ベストプラクティスに反している処理を行っている場合は 問題が起きる事がある • 特に sync over async で問題が発生しやすい

Slide 134

Slide 134 text

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 が実行されない

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

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

Slide 137

Slide 137 text

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 であったため

Slide 138

Slide 138 text

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 であったため

Slide 139

Slide 139 text

Reflection 138

Slide 140

Slide 140 text

Reflection 139 .NET 8 で UnsafeAccessorAttribute が導入 • Reflection なしに public でないメンバに無理やりアクセスする仕組み • だたし制約があった

Slide 141

Slide 141 text

Reflection 140 UnsafeAccessorAttribute の制約 その1 • 別 assembly の private type に対して無力

Slide 142

Slide 142 text

Reflection 141 UnsafeAccessorAttribute の制約 その1 • 別 assembly の private type に対して無力

Slide 143

Slide 143 text

Reflection 142 UnsafeAccessorAttribute の制約 その2 • static type のメンバにアクセスできない

Slide 144

Slide 144 text

Reflection 143 UnsafeAccessorAttribute の制約 その2 • static type のメンバにアクセスできない

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

Reflection 146 .NET 10 で UnsafeAccessorTypeAttribute の導入

Slide 148

Slide 148 text

Reflection 147 .NET 10 で UnsafeAccessorTypeAttribute の導入 Assembly A

Slide 149

Slide 149 text

Reflection 148 .NET 10 で UnsafeAccessorTypeAttribute の導入 Assembly A Assembly B

Slide 150

Slide 150 text

Reflection 149 .NET 10 で UnsafeAccessorTypeAttribute の導入 Assembly A Assembly B 別 Assembly の private type に対して [UnsafeAccessorType(“type名, assembly 名”)] を 用いることでアクセス可能に

Slide 151

Slide 151 text

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 が消えたので高速化!

Slide 152

Slide 152 text

Collections 151

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

• たとえば List を具象型のまま foreach するのは効率的 • GetEnumerator() は構造体を返すため効率的 • 一方で List を IEnumerable として扱うと foreach した時 IEnumerator を介してしまうので非効率 • boxing による allocation の発生 • interface dispatch の発生 Collections 153 Enumeration の効率化を図りたい これらは構造体を直接扱っている場合は発生しない課題

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

• JIT は特定のメソッドで最も頻繁に扱われる具象型を特定する • 特定した具象型に対して特化したコードを生成する • Devirtualization / Inlining / Object stack allocation • .NET 10 では T[], List に対してより積極的に最適化が行われる • さらに T[], List 以外でもより最適化が効くよう .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

Slide 157

Slide 157 text

Collections 156 コレクションの長さによって最適化具合が変わってしまう (.NET 9)

Slide 158

Slide 158 text

Collections 157 コレクションの長さによって最適化具合が変わってしまう (.NET 9)

Slide 159

Slide 159 text

Collections 158 コレクションの長さによって最適化具合が変わってしまう (.NET 9) コレクションが長いと Zero allocation ではなくなってしまう

Slide 160

Slide 160 text

Collections 159 なぜコレクションの要素数が増加すると allocation が発生してしまうのか?

Slide 161

Slide 161 text

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

Slide 162

Slide 162 text

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

Slide 163

Slide 163 text

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

Slide 164

Slide 164 text

Collections 163 .NET 10 では MoveNext を再実装 https://github.com/dotnet/runtime/blob/v10.0.0/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs#L1203 MoveNextRare は存在しない

Slide 165

Slide 165 text

Collections 164 MoveNext を再実装した結果 https://speakerdeck.com/nenonaninu/dot-net-8-deji-ding-deyou-xiao-ninatuta-dynamic-pgo-nituite?slide=19

Slide 166

Slide 166 text

Collections 165 MoveNext を再実装した結果 https://speakerdeck.com/nenonaninu/dot-net-8-deji-ding-deyou-xiao-ninatuta-dynamic-pgo-nituite?slide=19 しかしそれでも要素数が増えると Zero allocation ではなくなってしまう

Slide 167

Slide 167 text

Collections 166 MoveNext を再実装した結果 https://speakerdeck.com/nenonaninu/dot-net-8-deji-ding-deyou-xiao-ninatuta-dynamic-pgo-nituite?slide=19 しかしそれでも要素数が増えると Zero allocation ではなくなってしまう なぜか? これは on-stack replacement (OSR) の影響

Slide 168

Slide 168 text

Collections 167 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 用の計測コードが含まれていない

Slide 169

Slide 169 text

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 で最適化されたコードが用いられるようになるため

Slide 170

Slide 170 text

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.Enumerator.Dispose ではなく IEnumerator.Dispose を呼び出してしまい allocation が発生してしまう

Slide 171

Slide 171 text

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.Enumerator.Dispose ではなく IEnumerator.Dispose を呼び出してしまい allocation が発生してしまう OSR についてはこちら

Slide 172

Slide 172 text

Collections 171 .NET 10 では JIT の最適化により OSR で発生する課題に対処 https://github.com/dotnet/runtime/pull/118461

Slide 173

Slide 173 text

Collections 172 .NET 10 では JIT の最適化により OSR で発生する課題に対処 https://github.com/dotnet/runtime/pull/118461 Zero allocation

Slide 174

Slide 174 text

Collections 173 .NET 10 では JIT の最適化により OSR で発生する課題に対処 Dynamic PGO 的に欠けている計測情報を 周辺の enumerator の使われ方に対する 計測情報で補って最適化するようになった https://github.com/dotnet/runtime/pull/118461 Zero allocation

Slide 175

Slide 175 text

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

Slide 176

Slide 176 text

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

Slide 177

Slide 177 text

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

Slide 178

Slide 178 text

Collections: Stack 177 Stack の enumerator の実装を .NET 9/10 で比較 .NET 9 .NET 10

Slide 179

Slide 179 text

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

Slide 180

Slide 180 text

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

Slide 181

Slide 181 text

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

Slide 182

Slide 182 text

Collections: Stack 181 Stack の enumeration のパフォーマンス比較 (.NET 9/10)

Slide 183

Slide 183 text

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

Slide 184

Slide 184 text

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

Slide 185

Slide 185 text

Collections: Queue 184 Queue の enumerator の実装を .NET 9/10 で比較 .NET 9 .NET 10

Slide 186

Slide 186 text

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

Slide 187

Slide 187 text

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

Slide 188

Slide 188 text

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

Slide 189

Slide 189 text

Collections: Queue 188 .NET 10 における Queue の MoveNext の最適化 .NET 10 .NET 9

Slide 190

Slide 190 text

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

Slide 191

Slide 191 text

Collections: Queue 190 .NET 10 における Queue の MoveNext の最適化 index が配列の長さ未満の場合 これは範囲チェックの分岐が吹き飛ぶので 分岐が1回で済む index が配列の長さを超えている場合 こちらは範囲チェックの分岐挟まるので 分岐が2回発生する .NET 10 .NET 9

Slide 192

Slide 192 text

Collections: Queue 191 Queue の enumeration に対するパフォーマンス比較

Slide 193

Slide 193 text

Collections: ConcurrentDictionary 192 ConcurrentDictionary の構造 • ConcurrentDictionary はバケットのコレクションになっている • 要素はバケットに保存されている • バケットのコレクションは linked list として実装されている • そのため総舐めする際には二重のループが必要になっている • バケットのコレクションのループ • バケットの中身のループ

Slide 194

Slide 194 text

Collections: ConcurrentDictionary 193 .NET 9 時点での ConcurrentDictionary.Enumerator の MoveNext は複雑

Slide 195

Slide 195 text

Collections: ConcurrentDictionary 194 .NET 9 時点での ConcurrentDictionary.Enumerator の MoveNext は複雑 Irreducible なループが発生してしまっている

Slide 196

Slide 196 text

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

Slide 197

Slide 197 text

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

Slide 198

Slide 198 text

Collections: ConcurrentDictionary 197 ConcurrentDictionary.Enumerator の MoveNext の最適化 .NET 9 .NET 10 Reducible な実装に書き換わった

Slide 199

Slide 199 text

Collections: ConcurrentDictionary 198 ConcurrentDictionary の enumeration に対する パフォーマンス比較 (.NET 9/10)

Slide 200

Slide 200 text

LINQ 199

Slide 201

Slide 201 text

LINQ 200 .NET 9 でも LINQ の劇的な改善が行われた • .NET 9 では IIListProvider / IPartition が排除された • 全て Iterator ベースの実装に刷新された • IIListProvider / IPartition の責務が Iterator に集約された • Iterator 自体は .NET 8 以前から存在する class • .NET 9 のタイミングで Iterator は高速化のために拡張された • interface dispatch ではなく virtual dispatch になった https://github.com/dotnet/runtime/pull/98969

Slide 202

Slide 202 text

LINQ 201 なぜ Iterator のような最適化を行うか?

Slide 203

Slide 203 text

LINQ 202 なぜ Iterator のような最適化を行うか? OrderBy した後 First を叩く場合 ソートする必要はなく 純粋に key が最小値の値を探索すればいいだけ

Slide 204

Slide 204 text

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

Slide 205

Slide 205 text

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

Slide 206

Slide 206 text

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

Slide 207

Slide 207 text

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

Slide 208

Slide 208 text

LINQ 207 .NET 10 では特に Contains が強化された • Contains が OrderBy, Distinct, Reverse 等のオペレータの 後続に続く場合、上流のオペレータの処理は無視できる

Slide 209

Slide 209 text

LINQ 208 Contains のパフォーマンス比較

Slide 210

Slide 210 text

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

Slide 211

Slide 211 text

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

Slide 212

Slide 212 text

LINQ 211 Shuffle().Take(n) のパフォーマンス

Slide 213

Slide 213 text

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

Slide 214

Slide 214 text

LINQ 213 Shuffle().Take(n).Contains() の最適化問題の具体的な問題 • 100 個の要素があり、目的の対象となる要素が 20 個含まれている • 100 個の内から 5 個ピックアップした時、 目的の対象が 1 個以上含まれている確率は? 1 − 80 100 × 79 99 × 78 98 × 77 97 × 76 96 1個も目的の要素が得られない確率を求めて1から引けばいい (義務教育レベル!)

Slide 215

Slide 215 text

LINQ 214 それでは Shuffle().Take(n).Contains() をコードに落とし込むと?

Slide 216

Slide 216 text

LINQ 215 それでは Shuffle().Take(n).Contains() をコードに落とし込むと? equalCount さえ数え上げてしまえば 実際に shuffle する必要はない!

Slide 217

Slide 217 text

LINQ 216 Shuffle().Take(n).Contains() のパフォーマンス

Slide 218

Slide 218 text

LINQ 217 UseSizeOptimizedLinq オプションの導入 • もともと System.Linq.dll のビルドには2パターン存在した • CoreCLR 向け • パフォーマンス優先 • アセンブリサイズは犠牲 • NativeAOT 向け • アプリケーションのアセンブリサイズ優先 • パフォーマンスは犠牲 .NET 10 からは パフォーマンスの高い LINQ を NativeAOT でも利用できる

Slide 219

Slide 219 text

おわり

Slide 220

Slide 220 text

References 219 • https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/ • https://github.com/dotnet/runtime • https://speakerdeck.com/nenonaninu/dot-net-8-deji-ding-deyou-xiao-ninatuta- dynamic-pgo-nituite • https://speakerdeck.com/nenonaninu/dot-net-9-nopahuomansugai-shan