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

CysharpのOSS群から見るModern C#の現在地

CysharpのOSS群から見るModern C#の現在地

Sansan x Cysharp

Yoshifumi Kawai

November 18, 2024
Tweet

More Decks by Yoshifumi Kawai

Other Decks in Technology

Transcript

  1. About Speaker 河合 宜文 / Kawai Yoshifumi / @neuecc Cysharp,

    Inc. - CEO/CTO 株式会社Cygamesの子会社として2018年9月設立 C#関連の研究開発/OSS/コンサルティングを行う Microsoft MVP for Developer Technologies(C#) since 2011 CEDEC AWARDS 2022エンジニアリング部門優秀賞 .NETのクラスライブラリ設計 改訂新版 監訳 50以上のOSSライブラリ開発(UniRx, UniTask, MessagePack C#, etc...) C#では世界でもトップレベルのGitHub Star(合計40000+)を獲得
  2. UniTask (★8308) Unity用のカスタム非同期ランタイム https://github.com/Cysharp/UniTask 独自の非同期処理システムを実装 TaskはThreadPoolで動くランタイム UniTaskはゲームエンジンに特化し ゲームループ上で動くランタイム async UniTask<string>

    DemoAsync() { var asset = await Resources.LoadAsync<TextAsset> await SceneManager.LoadSceneAsync("scene2 "); await UniTask.Delay(TimeSpan.FromSeconds(1 0 ), ig await UniTask.NextFrame(); var task1 = GetTextAsync(UnityWebRequest.Get("ht var task2 = GetTextAsync(UnityWebRequest.Get("ht var task3 = GetTextAsync(UnityWebRequest.Get("ht var (google, bing, yahoo) = await UniTask.WhenAl return (asset as TextAsset)?.text; } Task、のようでTaskじゃ ない独自の生態系
  3. 非同期ランタイムの差し替え await可能な型 GetAwaiter()を実装する async関数の戻り値にできる型 AsyncMethodBuilderを実装する(C# 7.0) コンパイラ生成のコードがカスタム実装のBuilderを呼ぶようにな るため、既存のTask用のBuilder(AsyncTaskMethodBuilderを完全にバ イパスできる) [AsyncMethodBuilder(typeof(AsyncUniTaskMethodBuilder<>))]

    public readonly struct UniTask<T> async UniTask<string> DemoAsync() { } 全体をUniTaskで統一する、という世界観をユーザーに強いるこ とが出来れば、性能面/利便性でフレームワークに完全に特化した カスタムの非同期ランタイムをユーザーに提供することができる このカスタマイズ性は言語的にもかなりイ ケてる部類に入る!C#すごいイイ! まぁ、現状、実用的な形でそれを実践して幅広く 利用されているランタイムはUniTaskぐらいしか ありませんが……(逆にUniTaskすごい)
  4. MagicOnion (★3869) C# CodeFirst API/Realtime RPC https://github.com/Cysharp/MagicOnion gRPC上に構築されたC#専用RPC クライアントはUnityにも対応 gRPC標準のProtocol

    Buffersではなく MessagePack for C#を使用することで .proto不要のCode First RPCを実現 実はかなり歴史が古い async対応の独自型テクニックも利用して手触 り向上(Task<UnaryResult<T>> よりも UnaryResult<T>のほうが書きやすい
  5. 2016-08-23 gRPC 1.0 MagicOnion 0.1.0 2016-12-11 2019-09-20 first stable release

    of gRPC for .NET googleによるC#実装も含む わずか4か月後に公開 (開発開始はgRPC 1.0リリース直後から) 更に4か月後の2017-04-26にゲームをリリース してMagicOnionがiOS/Androidで実稼働 その数十年後にようやく MicrosoftがgRPCの価値に気付く
  6. RPC Generation 言語の違うREST Response型を別々 に書く APIクライアント を手書きする (メンテナンスも 大変で気合が必要、 何気に多い)

    中間IDLを書く そこからクライア ント・レスポンス 型自動生成 (←を嫌う時によ くある構成、これ が最先端だと多く の人が勘違いして いる) サービスを普通に 書く、そこからク ライアントをコマ ンドラインツール で自動生成、リク エスト・レスポン ス型は同一言語で 共有 サービスを普通に 書く、そこからク ライアントをコン パイル時Source Generatorで静的 生成、リクエス ト・レスポンス型 は同一言語で共有 普通のREST JsonSchema/gRPC MagicOnion v6 MagicOnion v7 MagicOnionは世代が遥か未来のRPC 世間は未だこの辺
  7. RPC Generation 言語の違うREST Response型を別々 に書く APIクライアント を手書きする (メンテナンスも 大変で気合が必要、 何気に多い)

    中間IDLを書く そこからクライア ント・レスポンス 型自動生成 (←を嫌う時によ くある構成、これ が最先端だと多く の人が勘違いして いる) サービスを普通に 書く、そこからク ライアントをコマ ンドラインツール で自動生成、リク エスト・レスポン ス型は同一言語で 共有 サービスを普通に 書く、そこからク ライアントをコン パイル時Source Generatorで静的 生成、リクエス ト・レスポンス型 は同一言語で共有 MagicOnionはC# RPCの決定版となるべく、 手触りが良いことを最重要視して、現在も 最前線で開発を進めています……!
  8. MemoryPack (★3345) The fastest serializer in C# https://github.com/Cysharp/MemoryPack C#に特化した実装 +

    バイナリ仕様により究極の速さを実現 MessagePack for C#(★5795)の限界を言語特化にすることで超えた 最近、.NET Foundationに参加しました! 実績(Visual StudioやSignalR, Blazorなど での採用)と相互運用性、実装の手堅さで はMessagePack for C#、エクストリーム パフォーマンスという点では MemoryPackという使い分け(?)
  9. パフォーマンスの秘訣 Source Generator 旧来のシリアライザーの高速化はIL.Emitが定番だったが いち早くSource Generatorに特化した Span<T>, IBufferWriter<T>,ReadOnlySequence<T> モダンI/O型の標準採用 System.Runtime.CompilerServices.Unsafe

    Span<T>と合わせて メモリを直接読み書きする あとは徹底的なアローケション避けや、 分岐の最小化、メソッド呼び出し回数 の削減など地道な努力……
  10. R3

  11. R3 (★2263) Reconstruction of Reactive Extensions https://github.com/Cysharp/R3 Rxベータ版からの超初期ユーザー(2011-)かつUnity版Rxである UniRx(2014- ★7098)の実装経験をもとに、Rxを現代的に再構築

    多くのプラットフォーム対応(Unity, Godot, Avalonia, WinUI3, etc...) より高機能(FrameOperators, AwaitOperators) より分かりやすく(async/await integration) よりアクティブなメンテナンスビリティ コードの複雑さから、dotnet/reactive既に ほとんどメンテ不能になってる……
  12. Stopwatch.GetElapsedTime from .NET 7 DateTime.UtcNow – nowは使わない! ある地点からの経過時間(TimeSpan)を取得するのに日付はいらない Stopwatch.StartNew(heap allocation)もいらない

    オレオレValueStopwatchもいらない 開始地点のlongをGetTimstampで取得して保持するだけ Stopwatch.GetTimestampは昔からあるけれど、2点のtimestampから TimeSpanを取得するGetElapsedTimeは.NET 7から……! (.NET 7以前でもMicrosoft.Bcl.TimeProviderを入れて TimeProvider.System.GetElapsedTimeを使うというハックもある!) 日付の取得 is not FREE ただのlongなのでStopwatchよりむしろ取り回しがいい
  13. ZString (★2082) Zero Allocation UTF8/UTF16 String Builder https://github.com/Cysharp/ZString 文字列周りの処理は、当時の.NETは多くの無駄があった 例えば数値型のStringBuilder.Appendでは

    netstandard2.0 -> 文字列化してからバッファに書き込み netstandard2.1 -> ISpanFormattable.TryFormatで直接書き込み Formatメソッドもobject argsによるboxingが避けられない、など Utf8関連でも文字列化->UTF8エンコードといった多くの無駄が発 生していた(.NET Core 3.1辺りで改善されて、doubleはGrisu3と いったアルゴリズムで直接書き込むようになった)
  14. Improvement Interpolated Strings C# 10.0でInterpolated Stringsが大幅改善された コンパイル時にInterpolatedStringHandler経由で処理する+適切な 一時バッファ量をコンパイル時推測することで、Zero Allocation UTF16

    String Builderが実現されている UTF16関連でのZStringのアドバンテージはあまりない UTF8周りはまだ未熟のため、新世代のライブラリを作った Utf8StringInterpolation(★157) https://github.com/Cysharp/Utf8StringInterpolation Utf8StreamReader(★213) https://github.com/Cysharp/Utf8StreamReader それつまりBetter ZString...... UTF8直読み書きはパフォー マンス上最重要!が、需要 は少ないみたい……
  15. ConsoleAppFramework (★1656) Source Generator based CLI Framework https://github.com/Cysharp/ConsoleAppFramework 最新版(v5)は大幅な破壊的変更によりSource Generatorベースに完

    全に作り変えられた ライブラリ自身も含めた追加の依存性ゼロ ゼロリフレクションによるNative AOT完全対応 ゼロアロケーションによる完全最適化した手書きと同じ実行速度 コールドスタートからの 実行速度も圧倒的!
  16. Ulid (★1334) Sortable GUID alternative https://github.com/Cysharp/Ulid 先頭48bitがTimestampなので ソート可能という性質を持つ データベースのIDとして使う場合などに、GUIDだ とランダム配置されるため性能劣化に繋がるが、

    UlidだとInsert時間で固まるため性能的に優位 (Optional)単独で生成時間が分かるのも便 利といえば便利(ユーザーに露出すると問 題がある場合もあるので注意) 実用的にはGUIDのランダム性 とほとんど遜色なし
  17. Guid.CreateVersion7(.NET 9) UUID Version 7 - RFC9562(2024-05) GUIDはUUIDのMicrosoft方言で、ほぼイコール Ulidの良くなかったところ UUIDと互換性のない文字列表現

    Guidと型が違う(当然だが既存システムとの相互運用性が劣る) CreateVersion7の良いところ よくもわるくもGuidそのもの CreateVersion7の良くないところ 生成速度がGuid + Timestampで先頭上書きなのでやや劣る 性質が違うのに型が同じとい うのは厳密には良くない。が、 現実的な利便性では同じこと のほうがプラス。 つまり、私的には.NET 9以降な らGuid v7のほうがお薦めです!
  18. public static class GuidEx { private const byte Variant1 0

    xxMask = 0 xC0 ; private const byte Variant1 0 xxValue = 0 x80 ; private const ushort VersionMask = 0 xF0 0 0 ; private const ushort Version7 Value = 0 x7 0 0 0 ; public static Guid CreateVersion7 () => CreateVersion7 (DateTimeOffset.UtcNow); public static Guid CreateVersion7 (DateTimeOffset timestamp) { // 普通にGUIDを作る Guid result = Guid.NewGuid(); // 先頭4 8bitをいい感じに埋める var unix_ts_ms = timestamp.ToUnixTimeMilliseconds(); // GUID layout is int _a; short _b; short _c, byte _d; Unsafe.As<Guid, int>(ref Unsafe.AsRef(ref result)) = (int)(unix_ts_ms >> 1 6 ); // _a Unsafe.Add(ref Unsafe.As<Guid, short>(ref Unsafe.AsRef(ref result)), 2 ) = (short)(unix_ts_ms); // _b ref var c = ref Unsafe.Add(ref Unsafe.As<Guid, short>(ref Unsafe.AsRef(ref result)), 3 ); c = (short)((c & ~VersionMask) | Version7 Value); ref var d = ref Unsafe.Add(ref Unsafe.As<Guid, byte>(ref Unsafe.AsRef(ref result)), 8); d = (byte)((d & ~Variant1 0 xxMask) | Variant1 0 xxValue); return result; } public static DateTimeOffset GetTimestamp(in Guid guid) { ref var p = ref Unsafe.As<Guid, byte>(ref Unsafe.AsRef(in guid)); var lower = Unsafe.ReadUnaligned<uint>(ref p); var upper = Unsafe.ReadUnaligned<ushort>(ref Unsafe.Add(ref p, 4 )); var time = (long)upper + (((long)lower) << 1 6 ); return DateTimeOffset.FromUnixTimeMilliseconds(time); } netstandard2.0向けにこういうの 作っちゃって、Guid v7に統一する のを選ぶかも Timestampの取得メソッドは標準 にはないので用意してもいいかも
  19. Endianに注意 GUIDの内部表現はLittleEndian ToByteArray(bool bigEndian)やTryWriteBytes(bool bigEndian)で 出力時のエンディアンを決定するという方式になっている 無指定の場合はリトルエンディアンになる v7はデータベースに格納する場合はBig推奨 タイムスタンプ部でSortするからバイナリ表現としてBigが適切 C#側のデータベースドライバーに注意

    GUIDがBigで書き込む設定になっていないと逆に性能劣化 mysqlconnector-netは接続文字列でGuidFormat=Binary16にする (デフォルトがそうなのでmysqlconnector-netは基本的に大丈夫) PostgreSQLも、npgsqlの標準のconverterがそうなってるので大丈夫。 Microsoft SQL Serverは悲しいことにダメで議論進行中。 https://github.com/dotnet/SqlClient/discussions/2999 新しいドライバのほう(Microsoft.Data.SqlClient)でダメなので、古いドライ バ(System.Data.SqlClient)はなおダメでしょう。