Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

B2B SaaSから見た最近のC#/.NETの進化

SansanTech
November 18, 2024

B2B SaaSから見た最近のC#/.NETの進化

■ イベント
イマドキのC#/.NET開発 〜最新の言語とフレームワークの使い方〜
https://sansan.connpass.com/event/333961/

■ 発表者
技術本部 Sansan Engineering Unit Data Hubグループ 藤原 雄介

■ Sansan Data Hubエンジニア 採用情報
https://media.sansan-engineering.com/datahub-engineer

■ Sansan Tech Blog
https://buildersbox.corp-sansan.com/

SansanTech

November 18, 2024
Tweet

More Decks by SansanTech

Other Decks in Technology

Transcript

  1. 3 ©Sansan, Inc. - 最近の .NET の変更のうち、プロダクトの現場から見て 「これは嬉しい!」というものをピックアップしてお届けします - そのため、「勝手に性能が上がって嬉しい」というような

    機能の紹介が多めになります - せっかくなので、なぜ勝手に性能が上がるのかを 少しだけ深堀していきます 本セッションについて
  2. 4 ©Sansan, Inc. ※詳細は省略しています (各マイクロサービスのデータストア等) Sansan Data Hubの全体像 管理用画面 エンリッチ用

    データソース データ書き出し先 データ取り込み元 データ連携用API エンリッチ処理群 書き出し処理群 取り込み処理群 コアデータ群
  3. 5 ©Sansan, Inc. - ランタイムやライブラリの高速化は純粋に嬉しい - 同じコストでより多くの処理を捌けるようになる - 記述が簡単になる、はそれに比べるとやや劣る -

    書き換えに見合うメリットが得られるか? > PRのレビューは楽になるかも - 統一性を失うデメリットを上回るか? - ライブラリの新機能系はケースバイケース - そこにはまる機能があるとは正直限らないので プロダクトの視点から嬉しいこと
  4. 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 の改善(不要なライトバリアの削減、コンパクション時の ソートアルゴリズムの改善、継続的な処理の改善(後述)) - 限定的なスタックオブジェクト割り付け(詳細後述) - サポートライフサイクル考えて上げると勝手に速くなるのは嬉しい ランタイム自身の高速化
  5. 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 例外処理の高速化
  6. 9 ©Sansan, Inc. - GCの対象にする必要のないマネージドオブジェクトを割り当てる領域 - おさらい:GCの動作(簡潔版) - オブジェクトのメモリはGCヒープに割り当てられる -

    GCヒープの空き領域が足りなくなると、ごみを探す。 具体的には、staticやローカル変数から参照されていないオブジェクトを GCヒープ全体をスキャンして探す(実際には複数の最適化手法を使う) - つまり、static readonly等をGCヒープに割り当てるのは単に無駄 - static readonly、typeof(…)の結果、Array.Empty<T>()の値 etc. - スキャンされるけど回収されることがない > ならGCの対象にならない領域に割り当てればいい=Non GC Heap - バージョンアップすると勝手に早くなるのはよい(再掲) GC: Non-GC Heap (.NET8)
  7. 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~)
  8. 11 ©Sansan, Inc. - アプリケーションの実行状況に合わせ、ヒープの数とGC実行のしきい値を 動的に調整するようにした - これまではセグメントやリージョンを拡大/縮小するだけで、ヒープの数は 固定(ワークステーション:1、サーバー:コア数)だった -

    DATAS により、ヒープ数が1~コア数までの可変となった > 1から始まるので、サーバーでは最初ヒープの排他制御によるスループット低下 が起こり得る - .NET 8ではオプトイン、.NET 9では既定で有効 - PaaSやコンテナを使って小さなプロセスをたくさん立ち上げている - 場合、メモリ使用量は割と重要で、地味に嬉しい(再掲) GC: DATAS (Dynamic Adaptation To Application Sizes)
  9. 12 ©Sansan, Inc. - スタック割り付けで大丈夫と判断される場合に、参照型のインスタンスを ヒープではなくスタックに割り付け、解放する最適化 - Hotspot JVMとかで割と前からあったやつ -

    .NET 9 時点ではボックス化にのみ対応 - 個人的には Span<byte> a = new byte[...] みたいなものもやってほしい - ボックス化は悪、とばかりに全て排するのは通常難しいので地味に嬉しい - 将来的により改善されていくことに期待できる スタックオブジェクト割り付け
  10. 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で有効
  11. 15 ©Sansan, Inc. - コードは書くよりも読む回数の方が多い - できる限り自然で(C#っぽく)、かつ意図が明確な方がよい - init プロパティと

    required プロパティによる「必須」メンバーの強制と 変更の可能性の抑制(ライトなイミュータブル化) - プライマリコンストラクターによる、「コンストラクター インジェクションしたいだけ」という意図の明確化 - extension type - explicit extension type をうまく使ったライトな Value Object の作成 > がしたかったけどオミットされた 意図の明確化
  12. 16 ©Sansan, Inc. プライマリコンストラクター class FooService { private readonly IDependency

    _bar; private readonly TimeProvider _timeprover; private readonly ILogger _logger; private readonly IOptionsMonitor<FooOptions> _options; public FooService(IDependency bar, TimeProvider timeProvider, ILogger<FooService> logger, IOptionsMonitor<FooOptions> options) { _bar = bar; : class FooService(IDependency bar, TimeProvider timeProvider, ILogger<FooService> logger, IOptionsMonitor<FooOptions> options) { public Boo DoSomething(...) { : } } コンストラクター インジェクションの意図が明白に
  13. 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~)
  14. 19 ©Sansan, Inc. - .NET 8で読み取りに最適化された専用コレクションが追加 - FrozenDictionary<TKey, TValue> -

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

    GCの新機能は理解しておかないとはまることもありそうなので注意 - 記述意図の明確化による保守性の向上 - 新しいライブラリの活用 - 来年の .NET 10 (LTS) に今から備えておきましょう - (C# だけど)dictionary literalとextension typeはよ まとめ