Slide 1

Slide 1 text

B2B SaaSから見た最近のC#/.NETの進化 Sansan株式会社 藤原 雄介 イマドキのC#/.NET開発 最新の言語とフレームワークの使い方

Slide 2

Slide 2 text

2 ©Sansan, Inc. 某SIerにて.NETベースのフレームワーク兼ミドルウェ ア、分散処理技術、IoT処理基盤の開発や適用支援、プ ロダクトビジネスの仕組みづくり等に従事。 その傍ら、.NET関連のいくつかの書籍の翻訳、OSSラ イブラリの公開を実施。 2023年4月にSansan株式会社へ入社し、アーキテクト としてSansan Data Hub全体の基盤やエンジニアリン グについて、ライブラリやフレームワークの中身に踏 み込むことを厭わず向き合っている。 藤原 雄介 (github: yfakariya) 自己紹介

Slide 3

Slide 3 text

3 ©Sansan, Inc. - 最近の .NET の変更のうち、プロダクトの現場から見て 「これは嬉しい!」というものをピックアップしてお届けします - そのため、「勝手に性能が上がって嬉しい」というような 機能の紹介が多めになります - せっかくなので、なぜ勝手に性能が上がるのかを 少しだけ深堀していきます 本セッションについて

Slide 4

Slide 4 text

4 ©Sansan, Inc. ※詳細は省略しています (各マイクロサービスのデータストア等) Sansan Data Hubの全体像 管理用画面 エンリッチ用 データソース データ書き出し先 データ取り込み元 データ連携用API エンリッチ処理群 書き出し処理群 取り込み処理群 コアデータ群

Slide 5

Slide 5 text

5 ©Sansan, Inc. - ランタイムやライブラリの高速化は純粋に嬉しい - 同じコストでより多くの処理を捌けるようになる - 記述が簡単になる、はそれに比べるとやや劣る - 書き換えに見合うメリットが得られるか? > PRのレビューは楽になるかも - 統一性を失うデメリットを上回るか? - ライブラリの新機能系はケースバイケース - そこにはまる機能があるとは正直限らないので プロダクトの視点から嬉しいこと

Slide 6

Slide 6 text

6 ©Sansan, Inc. - ランタイムをバージョンアップしてくれると勝手に高速化してくれる - InternalCallの最適化(FCall -> QCall) > 参考:https://github.com/dotnetreadingjp/coreclr-botr-jp/blob/master/botr/mscorlib.md - 処理の SIMD 化(例:Span.Count) - 不要な volatile の削減 - リフレクションの最適化、Type のメンバーの JIT Intrinsic 化 - 例外処理(詳細後述) - GC の改善(不要なライトバリアの削減、コンパクション時の ソートアルゴリズムの改善、継続的な処理の改善(後述)) - 限定的なスタックオブジェクト割り付け(詳細後述) - サポートライフサイクル考えて上げると勝手に速くなるのは嬉しい ランタイム自身の高速化

Slide 7

Slide 7 text

ランタイムによる自動的な高速化に 少し踏み込む

Slide 8

Slide 8 text

8 ©Sansan, Inc. - .NET 8から入った新しい例外処理 - CoreRTベースの実装の移植 > これまでは Win32 の RaiseExceptionやlibunwindによる処理だが、 汎用的でオーバーヘッドが大きかった > JVMの10倍くらい遅かった > 必要な処理のみに絞った移植で3~4倍高速に - .NET 8ではオプトイン、.NET 9 から既定で有効に - .NET 9でグローバルスピンロックがなくなりさらに高速に - なんだかんだ言って例外の発生はそれなりにある(一時的なものを含む) ので、勝手に高速化されるのは良い - 詳細:https://github.com/dotnet/runtime/issues/77568 例外処理の高速化

Slide 9

Slide 9 text

9 ©Sansan, Inc. - GCの対象にする必要のないマネージドオブジェクトを割り当てる領域 - おさらい:GCの動作(簡潔版) - オブジェクトのメモリはGCヒープに割り当てられる - GCヒープの空き領域が足りなくなると、ごみを探す。 具体的には、staticやローカル変数から参照されていないオブジェクトを GCヒープ全体をスキャンして探す(実際には複数の最適化手法を使う) - つまり、static readonly等をGCヒープに割り当てるのは単に無駄 - static readonly、typeof(…)の結果、Array.Empty()の値 etc. - スキャンされるけど回収されることがない > ならGCの対象にならない領域に割り当てればいい=Non GC Heap - バージョンアップすると勝手に早くなるのはよい(再掲) GC: Non-GC Heap (.NET8)

Slide 10

Slide 10 text

10 ©Sansan, Inc. - .NET 7から既定でONになっているが、LTSで初めて使う人も多いはず - OSからのGC用のメモリ領域の確保はバルクで行われる - そうすることで、メモリ確保のオーバーヘッドを下げたり、 連続した領域を確保できるようにする - .NET 6まではセグメント(x64 Server GCでは1GB)単位 > メモリを使うと階段状に増減する - .NET 7からはリージョン(4MB)単位 > 緩やかに増減する(はず) - さらに、リージョン単位で世代の昇格/降格が行われる > 世代別GCとしての効率が向上する、くらいの理解でOK - PaaSやコンテナを使って小さなプロセスをたくさん立ち上げている場合、メモ リ使用量は割と重要で、地味に嬉しい GC regions(.NET 7~)

Slide 11

Slide 11 text

11 ©Sansan, Inc. - アプリケーションの実行状況に合わせ、ヒープの数とGC実行のしきい値を 動的に調整するようにした - これまではセグメントやリージョンを拡大/縮小するだけで、ヒープの数は 固定(ワークステーション:1、サーバー:コア数)だった - DATAS により、ヒープ数が1~コア数までの可変となった > 1から始まるので、サーバーでは最初ヒープの排他制御によるスループット低下 が起こり得る - .NET 8ではオプトイン、.NET 9では既定で有効 - PaaSやコンテナを使って小さなプロセスをたくさん立ち上げている - 場合、メモリ使用量は割と重要で、地味に嬉しい(再掲) GC: DATAS (Dynamic Adaptation To Application Sizes)

Slide 12

Slide 12 text

12 ©Sansan, Inc. - スタック割り付けで大丈夫と判断される場合に、参照型のインスタンスを ヒープではなくスタックに割り付け、解放する最適化 - Hotspot JVMとかで割と前からあったやつ - .NET 9 時点ではボックス化にのみ対応 - 個人的には Span a = new byte[...] みたいなものもやってほしい - ボックス化は悪、とばかりに全て排するのは通常難しいので地味に嬉しい - 将来的により改善されていくことに期待できる スタックオブジェクト割り付け

Slide 13

Slide 13 text

即効性のある記述の改善

Slide 14

Slide 14 text

14 ©Sansan, Inc. - 特にテストコードで、テストの期待値や入力以外の情報はノイズになる - リテラルを簡潔に書けると嬉しい - 例: コレクションリテラル var result = DoSomething( ... new []{ 1, 2, 3, 4, 5 }); result.Should().Equal(new []{ 3, 5, 7, 9 })); var result = DoSomething( ... [1, 2, 3, 4, 5]); result.Should().Equal([3, 5, 7, 9])); 特に記述が込み入りがちな Assertionで有効

Slide 15

Slide 15 text

15 ©Sansan, Inc. - コードは書くよりも読む回数の方が多い - できる限り自然で(C#っぽく)、かつ意図が明確な方がよい - init プロパティと required プロパティによる「必須」メンバーの強制と 変更の可能性の抑制(ライトなイミュータブル化) - プライマリコンストラクターによる、「コンストラクター インジェクションしたいだけ」という意図の明確化 - extension type - explicit extension type をうまく使ったライトな Value Object の作成 > がしたかったけどオミットされた 意図の明確化

Slide 16

Slide 16 text

16 ©Sansan, Inc. プライマリコンストラクター class FooService { private readonly IDependency _bar; private readonly TimeProvider _timeprover; private readonly ILogger _logger; private readonly IOptionsMonitor _options; public FooService(IDependency bar, TimeProvider timeProvider, ILogger logger, IOptionsMonitor options) { _bar = bar; : class FooService(IDependency bar, TimeProvider timeProvider, ILogger logger, IOptionsMonitor options) { public Boo DoSomething(...) { : } } コンストラクター インジェクションの意図が明白に

Slide 17

Slide 17 text

手軽に対応できそうな ライブラリの変更

Slide 18

Slide 18 text

18 ©Sansan, Inc. - Search Values とは - 検索のアルゴリズムとして、検索する値を一度に渡せると効率的な場合がある > たとえば線形検索の場合 - Memory/SpanのXxxAnyメソッドで指定可能 > ContainsAny, IndexOfAny, LastIndexOfAny - 値が2~3個の場合は、直接指定するオーバーロードあり - AnyExcept(指定したものたち以外), AnyInRange(指定した範囲内), AnyExceptInRange(指定した範囲外)もある > .NET 9でSplitAnyも追加 - 正規表現の内部でも使用される > OR表現とか > .NET 9では大文字小文字を区別しないときにも活用 - 正規表現が勝手に高速化しているのは嬉しい - 知っていれば使えるので活用していきたい SearchValues (.NET 7~)

Slide 19

Slide 19 text

19 ©Sansan, Inc. - .NET 8で読み取りに最適化された専用コレクションが追加 - FrozenDictionary - FrozenSet - 作成にコストがかかり、変更はできない代わりに、キー参照や列挙に 最適化されたデータ構造/アルゴリズムを採用 - 変更を検知しない、文字列やInt32キー型に特化した実装、要素数が 少ない場合には配列ベースの実装を使用 - 置き換えは必要だが、「理由がなければこれを使っておけ」とはいえる - 意外と読み取り専用のディクショナリは存在する 読み取り最適化コレクション(.NET 8~)

Slide 20

Slide 20 text

まとめ

Slide 21

Slide 21 text

21 ©Sansan, Inc. - 最近の .NET の言語やランタイムの進化による恩恵 - ランタイムの進化による(勝手な)性能向上 > GCの新機能は理解しておかないとはまることもありそうなので注意 - 記述意図の明確化による保守性の向上 - 新しいライブラリの活用 - 来年の .NET 10 (LTS) に今から備えておきましょう - (C# だけど)dictionary literalとextension typeはよ まとめ