前書き
この記事は、2023のUnityアドカレの12/12の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい!
はじめに
C++を触ったことがある方ならBoostというライブラリをご存知かと思います。Boostは、C++の新しい機能が、標準化された組み込みライブラリに組み込まれる前の試験段階のようなものです。ベータ版みたいな。(厳密にはBoostにあるものが必ず標準になるわけではないです)
C#にも、実はそのようなライブラリがあります。
その名もDotNext!
このライブラリもBoostと同じように、BCL(C#の標準組み込みライブラリ)の発展版として存在しています。また、これはdotnetの公式リポジトリで管理されており、NuGetから利用することもできます。
本記事では、このDotNextに含まれる機能の一部をご紹介しましょう!
ゼロアロケーションList(BufferWriterSlim<T>)
これは、Listみたいなクラスです。内部に連続したバッファと末尾を表すCountを持ち、Addが可能なバッファビューです。
Listとの違いは、Listは内部にArrayを抱えており、つまりManagedヒープを使うのですが、BufferWriterSlimはスタックを利用します。そのため、BufferWriterSlim自体もref構造体となっており、基本的にメソッドの外に出ることができません。一方で、スタックを使うのでゼロアロケーションです。
using var list = new BufferWriterSlim<byte>(stackalloc byte[2]);
list.Add(146); // listOnStack.Count: 1
list.Add(81); // listOnStack.Count: 2
また、コンストラクタにはMemoryAllocatorを渡すことが可能で、これが渡された場合、スタックバッファをオーバーフローしそうな際に、MemoryAllocatorで新しいバッファを確保しフォールバックしてくれます。Disposableなのはそのためです。
StringBuilderの非アロケーション版(BufferHelpers.BufferWriterSlim)
BufferWriterSlim<char>
をStringBuilderのように扱うことができる拡張クラスです。
using var sb = new BufferWriterSlim<char>(stackalloc char[128]);
sb.Write("Hello, world!");
sb.WriteLine();
sb.WriteFormattable(184, "X");
Console.WriteLine(sb.ToString()); // "Hello, world!\n184"
既存クラスのオブジェクトに任意のデータを追加(UserDataSlot)
UserDataStorageという領域にオブジェクトインスタンスの追加データを保存します。UserDataStorageはDictionary<object, TExData>
のようなstaticな領域を持っています。
Setする際に、BackingStorageが生成され、BackingStorageは、オブジェクトインスタンスのハッシュからインデックスを計算します。これはまさにDictionary的な動きですよね。
UserDataSlot<string> exDataName = new();
var obj = new object();
obj.GetUserData().Set(addData, "Hoge Object"); // 書き込み
var name = obj.GetUserData().GetOrSet(addData); // 読み取り
Console.WriteLine(name); // "Hoge Object"
BoxingしないEnum.HasFlags
(Intrinsics.HasFlags
)
C#のEnumは、まさかの参照型だったりします。Enumは具体型によってサイズが違う(byte/short/int/longから派生させられる)ので仕方ない部分もありますが…
そのせいで、Enumを引数に取るメソッドではもれなくBoxingが発生します。特に問題になるのが、Enum.HasFlags
です。これは、単純なビット&演算のはずなので高速な動作を期待されてしまいますが、実はBoxingが発生するという罠です。(とはいえランタイムによって最適化が入ったり入らなかったりした気もします)
このIntrinsics.HasFlags
を使えばBoxingを発生させずに、フラグの判定をできます。
var flags = BindingFlags.Static | BindingFlags.Public;
var isStatic = Intrinsics.HasFlags(flags, BindingFlags.Static);
リストセグメント
ArraySegmentはあるのに、ListSegmentは存在しません。なのでDotNextではこれを追加します。
var list = new List<int> { 157, 75, -45 };
ListSegment<string> slice = list.Slice(1..); // [75, -45]
パフォーマンスを気にするなら、CollectionsMarshal.AsSpan<T>
の方が需要が多いかもしれません。こちらは、Spanなので様々な最適化の恩恵を受けることもできますし、何よりBCLに入っています。ただし、Spanなのでメソッドの外に原則洩らせないし、MarshalなのでUnsafeな操作な点は注意です。
var list = new List<int> { 157, 75, -45 };
Span<int> slice = CollectionsMarshal.AsSpan(list)[1..]; // [75, -45]
まとめ
他にも色々な機能がありますが、本記事ではこれぐらいにしたいと思います。(Part2は検討中)
C#はかつて、「パフォーマンスなんて二の次だぜ!」というような雰囲気でしたが、最近(.NET5~)はかなりパフォーマンス重視の機能が追加されてきています。DotNextでも、紹介した機能や他の機能でもパフォーマンス関連の機能が多めな気もします。その分、初心者に優しくなくなってきてしまっているのかもしれませんが、これもプログラミング言語の宿命でしょうか。
というわけ、DotNextを活用、あるいはその考え方を最適化の参考にしていただければ幸いです。