Slide 1

Slide 1 text

CysharpのOSSから見る Modern C#の現在地 2024-11-18 Cysharp x Sansan Event Yoshifumi Kawai / Cysharp, Inc.

Slide 2

Slide 2 text

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+)を獲得

Slide 3

Slide 3 text

https://github.com/Cysharp/ GitHubスター総数30000以上!

Slide 4

Slide 4 text

上位10個(合計27000スター)を 見ていきます! 技術的に見るべきところが多いとか先端度が高いとか はスター数とはあまり関係ないのですが、今回は分か りやすくTOP10ランキングからの紹介でいきます!

Slide 5

Slide 5 text

UniTask

Slide 6

Slide 6 text

UniTask (★8308) Unity用のカスタム非同期ランタイム https://github.com/Cysharp/UniTask 独自の非同期処理システムを実装 TaskはThreadPoolで動くランタイム UniTaskはゲームエンジンに特化し ゲームループ上で動くランタイム Task、のようでTaskじゃ ない独自の生態系

Slide 7

Slide 7 text

非同期ランタイムの差し替え await可能な型 GetAwaiter()を実装する async関数の戻り値にできる型 AsyncMethodBuilderを実装する(C# 7.0) コンパイラ生成のコードがカスタム実装のBuilderを呼ぶようにな るため、既存のTask用のBuilderを完全にバイパスできる これはたまによくやるパターン ちょっと(かなり)珍しい

Slide 8

Slide 8 text

非同期ランタイムの差し替え await可能な型 GetAwaiter()を実装する async関数の戻り値にできる型 AsyncMethodBuilderを実装する(C# 7.0) コンパイラ生成のコードがカスタム実装のBuilderを呼ぶようにな るため、既存のTask用のBuilderを完全にバイパスできる 全体をUniTaskで統一する、という世界観をユーザーに強いるこ とが出来れば、性能面/利便性でフレームワークに完全に特化した カスタムの非同期ランタイムをユーザーに提供することができる このカスタマイズ性は言語的にもかなりイ ケてる部類に入る!C#すごいイイ! まぁ、現状、実用的な形でそれを実践して幅広く 利用されているランタイムはUniTaskぐらいしか ありませんが……(逆にUniTaskすごい)

Slide 9

Slide 9 text

MagicOnion

Slide 10

Slide 10 text

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のほうが書きやすい

Slide 11

Slide 11 text

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の価値に気付く

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

RPC Generation 言語の違うREST Response型を別々 に書く APIクライアント を手書きする (メンテナンスも 大変で気合が必要、 何気に多い) 中間IDLを書く そこからクライア ント・レスポンス 型自動生成 (←を嫌う時によ くある構成、これ が最先端だと多く の人が勘違いして いる) サービスを普通に 書く、そこからク ライアントをコマ ンドラインツール で自動生成、リク エスト・レスポン ス型は同一言語で 共有 サービスを普通に 書く、そこからク ライアントをコン パイル時Source Generatorで静的 生成、リクエス ト・レスポンス型 は同一言語で共有 MagicOnionはC# RPCの決定版となるべく、 手触りが良いことを最重要視して、現在も 最前線で開発を進めています……!

Slide 14

Slide 14 text

MemoryPack

Slide 15

Slide 15 text

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という使い分け(?)

Slide 16

Slide 16 text

パフォーマンスの秘訣 Source Generator 旧来のシリアライザーの高速化はIL.Emitが定番だったが いち早くSource Generatorに特化した Span, IBufferWriter,ReadOnlySequence モダンI/O型の標準採用 System.Runtime.CompilerServices.Unsafe Spanと合わせて メモリを直接読み書きする あとは徹底的なアローケション避けや、 分岐の最小化、メソッド呼び出し回数 の削減など地道な努力……

Slide 17

Slide 17 text

R3

Slide 18

Slide 18 text

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既に ほとんどメンテ不能になってる……

Slide 19

Slide 19 text

TimeProviderの全面採用 そもそもR3は存在そのものがModern C#なのだが? SystemClock.Now的な利用だけではなく、 DateTimeOffset, TimeZoneInfo, Stopwatch, Timer の抽象層になっている 標準で用意されてるFakeTimeProviderも便利

Slide 20

Slide 20 text

TimeProvider in R3 カスタムTimeProvider作りまくり 超大量のメソッド内利用

Slide 21

Slide 21 text

Use Timestamp DateTimeOffsetやStopwatchそのものは使わず、 GetTimestampで高解像度タイマー(Windowsで はQueryPerformanceCounter)から時間を取得 GetElapsedTimeでタイムスタンプ間 から経過時刻のTimeSpanを算出する

Slide 22

Slide 22 text

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よりむしろ取り回しがいい

Slide 23

Slide 23 text

ZString

Slide 24

Slide 24 text

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と いったアルゴリズムで直接書き込むようになった)

Slide 25

Slide 25 text

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直読み書きはパフォー マンス上最重要!が、需要 は少ないみたい……

Slide 26

Slide 26 text

ConsoleAppFramework

Slide 27

Slide 27 text

ConsoleAppFramework (★1656) Source Generator based CLI Framework https://github.com/Cysharp/ConsoleAppFramework 最新版(v5)は大幅な破壊的変更によりSource Generatorベースに完 全に作り変えられた ライブラリ自身も含めた追加の依存性ゼロ ゼロリフレクションによるNative AOT完全対応 ゼロアロケーションによる完全最適化した手書きと同じ実行速度 コールドスタートからの 実行速度も圧倒的!

Slide 28

Slide 28 text

マクロ的Source Generator メソッド呼び出しの中 身から生成する 多くのSource Generatorは属性ベースで動作します が、RustにはAttribute-like macrosとFunction-like macrosという分類があり、これはfunction-likeなス タイルとして着想を得た

Slide 29

Slide 29 text

MasterMemory

Slide 30

Slide 30 text

MasterMemory (★1534) Readonly In-Memory Database https://github.com/Cysharp/MasterMemory マスターデータ(実行中変動が(ほぼ)ないデータ)の特性から、 起動時にインメモリに全て読み込むことで結果的に省メモリ+高 速動作が可能になるという発想のデータベース Roslynを使ったコード生成が(当時と しては)秘訣だったのですが、今と なっては手法が古いのでSource Generatorベースに改修中......

Slide 31

Slide 31 text

MessagePipe

Slide 32

Slide 32 text

MessagePipe (★1438) High performance in-memory messaging https://github.com/Cysharp/MessagePipe DIベースのインメモリイベント処理フレームワーク 速い!それとasync対応も嬉し い!DI対応はフレームワーク (ASP .NET とかVContainerを使っ てる状態だとかなり嬉しい!

Slide 33

Slide 33 text

DI First Microsoft.Extensions.DependencyInjection よくも悪くも、今の .NET のフレームワークは全てがDIベース あらゆるフレームワークが Generic Host(Microsoft.Extensions.Hosting) の上で構築されているため そこに全面的に乗っかるパターン と、あえてそこから逸脱する (ConsoleAppFramework v5)と いった試みもしている

Slide 34

Slide 34 text

Ulid

Slide 35

Slide 35 text

Ulid (★1334) Sortable GUID alternative https://github.com/Cysharp/Ulid 先頭48bitがTimestampなので ソート可能という性質を持つ データベースのIDとして使う場合などに、GUIDだ とランダム配置されるため性能劣化に繋がるが、 UlidだとInsert時間で固まるため性能的に優位 (Optional)単独で生成時間が分かるのも便 利といえば便利(ユーザーに露出すると問 題がある場合もあるので注意) 実用的にはGUIDのランダム性 とほとんど遜色なし

Slide 36

Slide 36 text

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のほうがお薦めです!

Slide 37

Slide 37 text

public static class GuidEx { private const byte Variant10xxMask = 0xC0; private const byte Variant10xxValue = 0x80; private const ushort VersionMask = 0xF000; private const ushort Version7Value = 0x7000; public static Guid CreateVersion7() => CreateVersion7(DateTimeOffset.UtcNow); public static Guid CreateVersion7(DateTimeOffset timestamp) { // 普通にGUIDを作る Guid result = Guid.NewGuid(); // 先頭48bitをいい感じに埋める var unix_ts_ms = timestamp.ToUnixTimeMilliseconds(); // GUID layout is int _a; short _b; short _c, byte _d; Unsafe.As(ref Unsafe.AsRef(ref result)) = (int)(unix_ts_ms >> 16); // _a Unsafe.Add(ref Unsafe.As(ref Unsafe.AsRef(ref result)), 2) = (short)(unix_ts_ms); // _b ref var c = ref Unsafe.Add(ref Unsafe.As(ref Unsafe.AsRef(ref result)), 3); c = (short)((c & ~VersionMask) | Version7Value); ref var d = ref Unsafe.Add(ref Unsafe.As(ref Unsafe.AsRef(ref result)), 8); d = (byte)((d & ~Variant10xxMask) | Variant10xxValue); return result; } public static DateTimeOffset GetTimestamp(in Guid guid) { ref var p = ref Unsafe.As(ref Unsafe.AsRef(in guid)); var lower = Unsafe.ReadUnaligned(ref p); var upper = Unsafe.ReadUnaligned(ref Unsafe.Add(ref p, 4)); var time = (long)upper + (((long)lower) << 16); return DateTimeOffset.FromUnixTimeMilliseconds(time); } netstandard2.0向けにこういうの 作っちゃって、Guid v7に統一する のを選ぶかも Timestampの取得メソッドは標準 にはないので用意してもいいかも

Slide 38

Slide 38 text

Endianに注意 GUIDの内部表現はLittleEndian ToByteArray(bool bigEndian)やTryWriteBytes(bool bigEndian)で 出力時のエンディアンを決定するという方式になっている 無指定の場合はリトルエンディアンになる v7はデータベースに格納する場合はBig推奨 タイムスタンプ部でSortするからバイナリ表現としてBigが適切 C#側のデータベースドライバーに注意 GUIDがBigで書き込む設定になっていないと逆に性能劣化 mysqlconnector-netは接続文字列でGuidFormat=Binary16にする (デフォルトがそうなのでmysqlconnector-netは基本的に大丈夫)

Slide 39

Slide 39 text

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)はなおダメでしょう。

Slide 40

Slide 40 text

ZLogger

Slide 41

Slide 41 text

ZLogger (★1286) Zero Allocation Text/Structured Logger https://github.com/Cysharp/ZLogger 速い&デフォルト設定が速い!

Slide 42

Slide 42 text

Extreme Interpolated String カスタムInteprolatedStringHandlerにより、 String(UTF16)を介さずにUTF8のロギング用バッ ファーに直接書き込みすることでゼロアロケー ション&超高速化

Slide 43

Slide 43 text

Conclusion

Slide 44

Slide 44 text

C#の可能性を切り開いていく 言語は進化する、ではライブラリは? 可能性は両輪で切り開いていかなければならない 言語が進化するなら、ライブラリも必然的に進化する 時には破壊的変更も厭わない……! 使いやすさとパフォーマンスを引き出す そのための新しいC#であり、新しい.NET C#をあらゆる言語においてベストな環境にすることに Cysharpは貢献しています……!

Slide 45

Slide 45 text

No content