Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

【Unite Tokyo 2019】Understanding C# Struct All Things

7,565 views

Published on

2019/9/25-6に開催されたUnite Tokyo 2019の講演スライドです。
河合 宜文(株式会社Cysharp)

こんな人におすすめ
・C#を極めたいエンジニア
・パフォーマンスに興味のあるエンジニア
・プログラミング言語マニア

受講者が得られる知見
・structに関する深い知識
・パフォーマンス向上のヒント
・C#の新しい文法と活用法

Unityのイベント資料はこちらから:
https://www.slideshare.net/UnityTechnologiesJapan/clipboards

Published in: Technology
  • Be the first to comment

【Unite Tokyo 2019】Understanding C# Struct All Things

  1. 1. Understanding C# Struct All Things Cysharp, Inc. Kawai Yoshifumi
  2. 2. About Speaker 2 — 河合 宜文 / Kawai Yoshifumi / @neuecc — Cysharp, Inc. – CEO/CTO — Microsoft MVP for Developer Technologies(C#) — 50以上のOSS公開(UniRx, MagicOnion, MessagePack for C#, etc..) — 株式会社Cysharp — 2019年9月, Cygamesの子会社として設立 — C#関連の研究開発/OSS/コンサルティングを行う — C#大統一理論(サーバー/クライアントともにC#で実装する)を推進
  3. 3. https://github.com/Cysharp OSS for Unity – GitHub/Cysharp 3 UniTask ★288 Provides an efficient async/await integration to Unity. RuntimeUnitTestToolkit ★87 CLI/GUI Frontend of Unity Test Runner to test on any platforms. RandomFixtureKit ★16 Fill random/edge-case value to target type for unit testing. MagicOnion ★1240 Unified Realtime/API Engine for .NET Core and Unity. MasterMemory ★407 Embedded Typed Readonly In-Memory Document Database for .NET Core and Unity.
  4. 4. https://github.com/neuecc OSS for Unity – GitHub/neuecc 4 LINQ-to-GameObject-for-Unity ★448 Traverse GameObject Hierarchy by LINQ. PhotonWire ★92 Typed Asynchronous RPC Layer for Photon. SerializableDictionary ★87 SerializableCollections for Unity. ReMotion ★27 Hyper Fast Reactive Tween Engine for Unity. UniRx ★3722 Reactive Extensions for Unity. MessagePack-CSharp ★2089 Extremely Fast MessagePack Serializer. ZeroFormatter ★1778 Infinitely Fast Deserializer. Utf8Json ★1352 Definitely Fastest JSON Serializer.
  5. 5. 6 The Evolution of C# Struct
  6. 6. MessagePack for C#(v2-preview) 7 public ref struct MessagePackWriter T Deserialize<T>(in ReadOnlySequence<byte> byteSequence) Span<byte> bytes = stackalloc byte[36]; internal ref partial struct SequenceReader<T> where T : unmanaged, IEquatable<T> public ref byte GetPointer(int sizeHint) ref Entry v = ref entry[0];
  7. 7. MessagePack for C#(v2-preview) 8 public ref struct MessagePackWriter T Deserialize<T>(in ReadOnlySequence<byte> byteSequence) Span<byte> bytes = stackalloc byte[36]; internal ref partial struct SequenceReader<T> where T : unmanaged, IEquatable<T> ref struct Span<T> = stackalloc in parameter where : unmanaged public ref byte GetPointer(int sizeHint) ref Entry v = ref entry[0]; ref return ref local
  8. 8. DOTS(昨日の基調講演より)
  9. 9. DOTS(昨日の基調講演より)
  10. 10. What’s new struct features 11 C# 7.2 C# 7.3 C# 8.0 in modifier on parameter ref readonly modifier on method returns, local readonly struct declaration ref struct declaration ref extension method stackalloc to Span reassign ref local additional generics constraints(unmanage d, Enum, Delegate) stackalloc initializer access fixed field without pinning readonly method Disposable ref structs Unmanaged constructed types https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/ C# 7.0 ref return statement ref local variables
  11. 11. 12 Which Unity Version should we support? Unity Version C# Version .NET Version Unity 2017.4 6.0 .NET 3.5/.NET 4.6 Unity 2018.2 6.0 .NET 4.x/Standard 2.0 Unity 2018.3 7.3 .NET 4.x/Standard 2.0 Unity 2018.4 7.3 .NET 4.x/Standard 2.0 Unity 2019.1 7.3 .NET 4.x/Standard 2.0
  12. 12. 13 Which Unity Version should we support? Unity Version C# Version .NET Version Unity 2017.4 6.0 .NET 3.5/.NET 4.6 Unity 2018.2 6.0 .NET 4.x/Standard 2.0 Unity 2018.3 7.3 .NET 4.x/Standard 2.0 Unity 2018.4 7.3 .NET 4.x/Standard 2.0 Unity 2019.1 7.3 .NET 4.x/Standard 2.0 2018.3以上一択、それ以下の バージョンはNot Supported
  13. 13. 14 Which Unity Version should we support? Unity Version C# Version .NET Version Unity 2017.4 6.0 .NET 3.5/.NET 4.6 Unity 2018.2 6.0 .NET 4.x/Standard 2.0 Unity 2018.3 7.3 .NET 4.x/Standard 2.0 Unity 2018.4 7.3 .NET 4.x/Standard 2.0 Unity 2019.1 7.3 .NET 4.x/Standard 2.0 2018.3から以下の言語に関するdefineが使える CSHARP_7_3_OR_NEWER 以下のどちらか選んだほうが定義される NET_4_6 NET_STANDARD_2_0
  14. 14. Struct is important for Performance! 15 — C# 7以降の急速なstruct強化はパフォーマンスのため — それは .NET Core でも、Unityでも — アプローチは異なれど、両者とも構造体をパフォーマンスのため活用している — 特にUnityの推すDOTS(Data Oriented Technology Stack)はstructの塊 — 今、全てを学び、備えよう public unsafe ref struct BlobBuilderArray<T> where T : struct public unsafe static ref T AsRef<T>(void* ptr) where T : struct public readonly struct BuildComponentDataToEntityLookupTask<TComponentData> : IDisposable where TComponentData : unmanaged, IComponentData, IEquatable<TComponentData> Unity ECSの中の C# 7.3表現
  15. 15. 16 The Basic of C# Memory
  16. 16. The Memory of C# 17 AppDomain(Managed) Thread Stack HeapThread Stack Unmanaged
  17. 17. The Memory of C# 18 AppDomain(Managed) Thread Stack HeapThread Stack Unmanaged ローカル変数はスタック領域に格納される ヒープ領域に確保されたデータは GCの管理化に入る UnityではC#管理外のメモリを扱う こともよくある(特にDOTS)
  18. 18. Let’s see memory layout 19 — SharpLabでメモリの中身を見よう! — https://sharplab.io/ — C# to IL, C# to C#, C# to ASMなど豊富な機能がある — Run と Inspectを組み合わせるとメモリの中身が見れる なお、組み込み型の場合、Unity(mono) とSharpLab(.NET Core)で中身が異なる ことがある場合に注意
  19. 19. Struct Memory Int(4バイト)のX, Y, Z(12バイト)が 素直にメモリ上(スタック)に並ぶ
  20. 20. Class Memory スタック上の変数はヒープ上 のアドレスを指す ヒープ上に確保されるメモリは管理 用のヘッダ/型情報 + 実データ
  21. 21. Pass by Reference/Pass by Value 22 — C#はデフォルトは全て「値渡し」 — つまりコピーされます — ローカル変数への代入もコピー (T x) (ref T x) class 参照の値渡し 参照の参照渡し struct 値の値渡し 値の参照渡し
  22. 22. スタック領域は大体この辺っぽい
  23. 23. 00 00 00 00 からそれっぽ いデータが入った気配
  24. 24. 引き続き、 y = x で同じデ ータが追加で入ったっぽい
  25. 25. 別のところにxとyがコピー されて入った気配
  26. 26. 戻りのzを受け取って全部 埋まった
  27. 27. int x int y int z コンパイル時(C# -> IL)の段 階で変数の置き場確保してお きます、的な
  28. 28. Structの基本的な原則 29 — 全てはコピーに気をつける、ということ — クラスとの違い、ほとんどの問題はコピーにより引き起こされる — 大きなサイズの構造体を(基本的には)作らない – IntPtr.Size(4 or 8バイト)以上は参照渡しに比べて大きいということになる – それだと制限キツすぎなので、一般には16バイト以下ぐらいを目安に — 変更可能な構造体を(基本的には)作らない – コピーされることによって、変更したつもりが変更されない – 一度は悩むVector3変更されない問題
  29. 29. Structの基本的な原則 30 — 全てはコピーに気をつける、ということ — クラスとの違い、ほとんどの問題はコピーにより引き起こされる — 大きなサイズの構造体を(基本的には)作らない – IntPtr.Size(4 or 8バイト)以上は参照渡しに比べて大きいということになる – それだと制限キツすぎなので、一般には16バイト以下ぐらいを目安に — 変更可能な構造体を(基本的には)作らない – コピーされることによって、変更したつもりが変更されない – 一度は悩むVector3変更されない問題 // position変えたつもりが変わらない! this.transform.position.Set(10f, 20f, 30f); // つまりこういうことだから this.transform.INTERNAL_get_position(out Vector3 value); value.Set(10f, 20f, 30f);
  30. 30. Structの基本的な原則 31 — 全てはコピーに気をつける、ということ — クラスとの違い、ほとんどの問題はコピーにより引き起こされる — 大きなサイズの構造体を(基本的には)作らない – IntPtr.Size(4 or 8バイト)以上は参照渡しに比べて大きいということになる – それだと制限キツすぎなので、一般には16バイト以下ぐらいを目安に — 変更可能な構造体を(基本的には)作らない – コピーされることによって、変更したつもりが変更されない – 一度は悩むVector3変更されない問題 // position変えたつもりが変わらない! this.transform.position.Set(10f, 20f, 30f); // つまりこういうことだから this.transform.INTERNAL_get_position(out Vector3 value); value.Set(10f, 20f, 30f); positionがプロパティなのが悪い (フィールドならコピーの問題は起きない) が、これはtransformのデータの実体は アンマネージドメモリ側(UnityEngine)にある ためという、Unityならではの悩みでもある (C#/C++越境がどうしても抱える話)
  31. 31. Boxed Struct ボックス化 スタック上の変数は ヒープのアドレス ヒープ上にクラスと 同様ヘッダが付いた うえで領域確保 アンボックスの毎に ここからスタックへ コピーする
  32. 32. Box化との戦い 33 — ボックス化はnewと一緒 — むしろアンボックスの頻度的により悪い — ボックス化が避けられないなら最初からクラスで作ることも選択肢 — インターフェイスへのキャストに気をつける – ジェネリクスを使って回避していく – enumの場合はEnum(参照型)も同様 public static class BoxedInt { public static readonly object Zero = 0; public static readonly object One = 1; public static readonly object MinusOne = -1; } ある程度決まった値が頻繁にボッ クス化されるようなら、先に作っ ておいて使い回すという技
  33. 33. Equalsの自動実装とBox化 34 — structはEqualsが実装されていない場合、自動的に以下のものが呼ばれる — めっちゃ遅い — Equalsは辞書のKeyにすると呼ばれる! そのため辞書のKeyにする構造体は IEquatable<T>とGetHashCodeの カスタム実装を必ず行うこと internal static bool DefaultEquals(object o1, object o2) { RuntimeType o1_type = (RuntimeType)o1.GetType(); RuntimeType o2_type = (RuntimeType)o2.GetType(); object[] fields; InternalEquals(o1, o2, out fields); for (int i = 0; i < fields.Length; i += 2) { object meVal = fields[i]; object youVal = fields[i + 1]; if (!meVal.Equals(youVal)) return false; } return true; } object, object比較のボクシング そもそもリフレクションで全フィ ールド比較(遅い)うえに、フィ ールドの戻り値もボクシング 原理主義的には可能なもの全てのStructにカスタム実装を入 れたほうがいい、ということになるけれど、あまりにも面倒 なので、さすがにそこはピンポイント(辞書のKeyになるもの だけ)でいいと思います
  34. 34. Struct Layout and Padding 単純計算ではbyte(1) + long(8) + int(4) = 13ですが、 アラインメント調整のため、最長の8にそれぞれが合 わせられて8 * 3 = 24バイトの確保になっている
  35. 35. Struct Layout and Padding StructLayoutやFieldOffsetによってレイアウ トはカスタマイズ可能。 structのデフォルトはSequential(宣言順) Autoに変えると、ZとXが詰められることで最 小の16バイトに縮む 参照型はstructと異なりデフォ ルトがAuto
  36. 36. Heap Layout: Array オブジェクトの共通ヘッダの後 ろに長さが付いてる データ領域には要素がそのまま順番に並ぶ。構造体な ら、値がそのまま順番に並んでいることになる(参照 型の場合はポインタが並んでいるため、実態のデータ を更に辿る必要がある)
  37. 37. 38 ref and readonly
  38. 38. Zero Allocation foreach in List<T> 39 — foreachは .GetEnumerator -> while(MoveNext()) に変換される — (ただし配列の場合はコンパイル時にILでforに変換される) — つまり IEnumerator が生成されてヒープに確保されている? — されるようでされない var list = new List<int>() { 1, 2, 3 }; foreach (var item in list) { /* do anything */ }
  39. 39. Mutable Struct is Evil but Useful 40 — 一時的な入れ物として使うものに向いてる public struct Enumerator : IEnumerator<T> { List<T> list; int index; int version; T current; } public struct BinaryReader { byte[] bytes; int offset; } List<T>は直接GetEnumeratorを呼べる 状況ではstruct List<T>.Enumerator を 返すためゼロアロケーション バイナリを読みすすめる際にReadXxx を呼ぶたびにoffsetを追加していくとい うステートを管理 局所的にしか使わないので classじゃなくてもいい
  40. 40. ref struct 41 — スタックにしか置けないという制約がref struct – 元々はSpan<T>(System.Memory, .NET Standard 2.0外部ライブラリ)のため – Span<T>は連続したメモリ領域のビューで、配列のように扱える(NativeArrayみたいな) – 今までポインタでしか扱えなかったstackallocを自然に扱えて便利 – しかしそれによってスタックにのみ確保したメモリ領域をヒープに移されると危険 – フィールドに置けない(ref structのfieldの場合のみ可)、ボクシングできない、インターフ ェイスを実装できない、ジェネリクスの型引数にできない、などの制約がある — ビュー的なものや一時的にしか使わない状態を持つものには適用しやすい – 制約が多いので無理に使おうとするとハマりますが…… Span<int> temp = stackalloc int[12];
  41. 41. internal ref struct TempList<T> { int index; T[] array; public ReadOnlySpan<T> Span => new ReadOnlySpan<T>(array, 0, index); public TempList(int initialCapacity) { this.array = ArrayPool<T>.Shared.Rent(initialCapacity); this.index = 0; } public void Add(T value) { if (array.Length <= index) { var newArray = ArrayPool<T>.Shared.Rent(index * 2); Array.Copy(array, newArray, index); ArrayPool<T>.Shared.Return(array, true); array = newArray; } array[index++] = value; } public void Dispose() { ArrayPool<T>.Shared.Return(array, true); // clear for de-reference all. } } ArrayPool(System.Buffers, Unityで は似たようなものを自作すれば……) から確保済み配列を取得し使う Disposeで返却 一時的にしか使わない配 列を都度確保せずプール から取得するための構造 (TempList<T>) プールを扱っているので、 寿命は明確に短くあって ほしいのでref struct
  42. 42. public void DoNanika(IEnumerable<int> idList) { var resources = idList.Select(x => Load(x)); // LINQの遅延実行により二回のLoadが走ってしまう // それを避けるために .ToList() するとそれはそれでListの無駄を感じる foreach (var item in resources) { /* nanika suru 1 */ } foreach (var item in resources) { /* nanika suru 2 */ } } public void DoNanika(IEnumerable<int> idList) { using var resources = idList.Select(x => Load(x)).ToTempList(); foreach (var item in resources) { /* nanika suru 1 */ } foreach (var item in resources) { /* nanika suru 2 */ } } usingだけで末尾で Disposeが便利 (C# 8.0から! Unityではまだ!) ここの中だけで使う一時配列はPoolから 取ってるのでアロケートなしで済んだ
  43. 43. Avoid the copy, Everywhere 44 — 全て ref で引き回せばいい、とはいうものの現実的ではない — あまりにも最悪な書き心地になる! — あるいは全てunsafeでポインタで取り回すという手も…… — Unity.Entitiesのソースコードはかなりそれに近い [StructLayout(LayoutKind.Sequential)] internal unsafe struct Archetype { public ArchetypeChunkData Chunks; public UnsafeChunkPtrList ChunksWithEmptySlots; public ChunkListMap FreeChunksBySharedComponents; public int EntityCount; public int ChunkCapacity; public int BytesPerInstance; public ComponentTypeInArchetype* Types; public int TypesCount; public int NonZeroSizedTypesCount; public int* Offsets; public int* SizeOfs; public int* BufferCapacities; public int* TypeMemoryOrder; public int* ManagedArrayOffset; public int NumManagedArrays; // ... まだまだいっぱい void AddArchetypeIfMatching( Archetype* archetype, EntityQueryData* query) (ECSより引用)とにかく巨大なStruct 全部ポインタで引き回すからOK(?) (ECSはネイティブメモリを使ったり色々 と固有の事情があるので一般論は適用で きない)
  44. 44. static void Normal(Vector3 v3) { } static void In(in Vector3 v3) { } static void Ref(ref Vector3 v3) { } Avoid the copy, Everywhere 45 そこで登場するのが新しいキーワード “in” (このコード例自体には何の意味もないです)
  45. 45. static void Normal(Vector3 v3) { } static void In(in Vector3 v3) { } static void Ref(ref Vector3 v3) { } Avoid the copy, Everywhere 46 Normal(v3); In(v3); Ref(ref v3); 呼び側のコード
  46. 46. static void Normal(Vector3 v3) { } static void In(in Vector3 v3) { } static void Ref(ref Vector3 v3) { } Avoid the copy, Everywhere 47 ldloc.0 call Noraml ldloca.0 call In ldloca.0 call Ref Normal(v3); In(v3); Ref(ref v3); 呼び側のIL
  47. 47. static void Normal(Vector3 v3) { } static void In(in Vector3 v3) { } static void Ref(ref Vector3 v3) { } Avoid the copy, Everywhere 48 ldloc.0 call Noraml ldloca.0 call In ldloca.0 call Ref 呼び方は普通と一緒なのに refと同じく参照渡しされる! Normal(v3); In(v3); Ref(ref v3); じゃあ全部 in でいいね! (にはならない)
  48. 48. static void Normal(Vector3 v3) { _ = v3.magnitude; } static void In(in Vector3 v3) { _ = v3.magnitude; } static void Ref(ref Vector3 v3) { _ = v3.magnitude; } Avoid the copy, Everywhere 49 ldarga.0 call get_magnitude ldarg.0 ldobj stloc.0 ldloca.0 call get_magnitude ldarg.0 call get_magnitude 呼ばれ側のIL
  49. 49. static void Normal(Vector3 v3) { _ = v3.magnitude; } static void In(in Vector3 v3) { _ = v3.magnitude; } static void Ref(ref Vector3 v3) { _ = v3.magnitude; } Avoid the copy, Everywhere 50 ldarga.0 call get_magnitude ldarg.0 ldobj stloc.0 ldloca.0 call get_magnitude ldarg.0 call get_magnitude 呼ばれ側のIL コピーされてる(防御的コピー) var v3_2 = v3; _ = v3_2.magnitude;
  50. 50. static void Normal(Vector3 v3) { _ = v3.magnitude; _ = v3.magnitude; } static void In(in Vector3 v3) { _ = v3.magnitude; _ = v3.magnitude; } static void Ref(ref Vector3 v3) { _ = v3.magnitude; _ = v3.magnitude; } Avoid the copy, Everywhere 51 ldarga.0 call get_magnitude ldarg.0 ldobj stloc.0 ldloca.0 call get_magnitude ldarg.0 call get_magnitude 呼ばれ側のIL 二回呼べばコピーもそのまま二つ ldarga.0 call get_magnitude ldarg.0 ldobj stloc.0 ldloca.0 call get_magnitude ldarg.0 call get_magnitude
  51. 51. Best practice to use `in` 52 — in はコンパイルすると([In][IsReadOnly]ref T t) になる — 読み取り専用のため、フィールドへの代入はできない — v3.x = 10.0f; // compile error — メソッド呼び出しは可能だが、中身が変わらない保証がないため防御的コピーが走る — そのため、うかつに多用すると防御的コピーにより、むしろ性能低下もありうる — もしそれがMutable Structだともはやわけわからないことに — (Vector3.magnitudeはプロパティなのでメソッド扱い) — 防御的コピーが走らない条件は – プロパティ/メソッドを呼ばないこと – あるいは readonly struct であること – readonly structは書き換わらないことが保証されるため防御的コピーが走らない
  52. 52. Best practice to use `in` 53 — in はコンパイルすると([In][IsReadOnly]ref T t) になる — 読み取り専用のため、フィールドへの代入はできない — v3.x = 10.0f; // compile error — メソッド呼び出しは可能だが、中身が変わらない保証がないため防御的コピーが走る — そのため、うかつに多用すると防御的コピーにより、むしろ性能低下もありうる — もしそれがMutable Structだともはやわけわからないことに — 防御的コピーが走らない条件は – プロパティ/メソッドを呼ばないこと – あるいは readonly struct であること – readonly structは書き換わらないことが保証されるため防御的コピーが走らない public readonly struct MyVector3 { public readonly float x; public readonly float y; public readonly float z; public MyVector3(float x, float y, float z) { this.x = x; this.y = y; this.z = z; } readonly structの条件は全てのフィ ールドがreadonlyであること ref readonly structもOK 原則inの引数はreadonly struct 中でフィールドしか絶対触らないと力 強く言えるならナシではない よくわからないなら基本使わない
  53. 53. readonly field(struct)の罠 54 — readonlyなfieldのstructにmutableな操作を行っても変更されない — よって、ミュータブルな操作を行うものはreadonly fieldにすべきではない — 原則的には「可能なものは」と言いたいところだけど Unityの場合、可能なものが多いので…… public class NantokaBehaviour : MonoBehaviour { public readonly Vector3 NanikanoVector3; public void Incr() { NanikanoVector3.Set( NanikanoVector3.x + 1f, NanikanoVector3.y + 1f, NanikanoVector3.z + 1f); } } 何回Incr呼んでも0, 0, 0のまま
  54. 54. 55 Manipulate Memory
  55. 55. 改めてStructとは 56 — メモリを単純にマッピングした構造 – そのことだけ意識すれば、あとはやりたい放題できる [StructLayout(LayoutKind.Sequential, Size = 16)] public struct Empty16 { } public struct LongLong { public long X; public long Y; } どちらも連続して16バイトの 領域を確保しているだけ、とい う意味(に読める) インデックス0とインデックス 8にアクセスしやすくしている だけ(という風に読める)
  56. 56. 57 // 先頭6バイトがTimestamp, 後ろ10バイトがランダムというUlidという仕様(ソート可能なGuidの代替) [StructLayout(LayoutKind.Explicit, Size = 16)] public struct Ulid { // TimestampとRandomnessのはじまりの部分だけ持っておく(最悪なくても別にいい) [FieldOffset(0)] byte timestamp0; [FieldOffset(6)] byte randomness0; public static Ulid NewUlid() { var memory = default(Ulid); // 16バイト確保 var timestampMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); ref var fisrtByte = ref Unsafe.As<long, byte>(ref timestampMilliseconds); Unsafe.Add(ref memory.timestamp0, 0) = Unsafe.Add(ref fisrtByte, 5); Unsafe.Add(ref memory.timestamp0, 1) = Unsafe.Add(ref fisrtByte, 4); Unsafe.Add(ref memory.timestamp0, 2) = Unsafe.Add(ref fisrtByte, 3); Unsafe.Add(ref memory.timestamp0, 3) = Unsafe.Add(ref fisrtByte, 2); Unsafe.Add(ref memory.timestamp0, 4) = Unsafe.Add(ref fisrtByte, 1); Unsafe.Add(ref memory.timestamp0, 5) = Unsafe.Add(ref fisrtByte, 0); Unsafe.WriteUnaligned(ref memory.randomness0, xorshift.NextULong()); Unsafe.WriteUnaligned(ref Unsafe.Add(ref memory.randomness0, 2), xorshift.NextULong()); return memory; } Ulid: [Timestamp(6), Randomness(10)] の実装 (System.Runtime .CompilerService s.Unsafeを利用、 Unityでも動きま すが別途参照は必 要、ポインタでや ってもいいです)
  57. 57. 58 // 先頭6バイトがTimestamp, 後ろ10バイトがランダムというUlidという仕様(ソート可能なGuidの代替) [StructLayout(LayoutKind.Explicit, Size = 16)] public struct Ulid { // TimestampとRandomnessのはじまりの部分だけ持っておく(最悪なくても別にいい) [FieldOffset(0)] byte timestamp0; [FieldOffset(6)] byte randomness0; public static Ulid NewUlid() { var memory = default(Ulid); // 16バイト確保 var timestampMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); ref var fisrtByte = ref Unsafe.As<long, byte>(ref timestampMilliseconds); Unsafe.Add(ref memory.timestamp0, 0) = Unsafe.Add(ref fisrtByte, 5); Unsafe.Add(ref memory.timestamp0, 1) = Unsafe.Add(ref fisrtByte, 4); Unsafe.Add(ref memory.timestamp0, 2) = Unsafe.Add(ref fisrtByte, 3); Unsafe.Add(ref memory.timestamp0, 3) = Unsafe.Add(ref fisrtByte, 2); Unsafe.Add(ref memory.timestamp0, 4) = Unsafe.Add(ref fisrtByte, 1); Unsafe.Add(ref memory.timestamp0, 5) = Unsafe.Add(ref fisrtByte, 0); Unsafe.WriteUnaligned(ref memory.randomness0, xorshift.NextULong()); Unsafe.WriteUnaligned(ref Unsafe.Add(ref memory.randomness0, 2), xorshift.NextULong()); return memory; } // メモリ領域をコピーすればおk。 // 文字列表現としてBase32エンコード(ToString)も同様に自分のメモリ粋から算出 public bool TryWriteBytes(Span<byte> destination) { if (destination.Length < 16) { return false; } Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(destination), this) return true; } 出力先としてbyte[]とstringがあればそれでいい それらはほとんどメモリコピーで実現する
  58. 58. Union 59 [StructLayout(LayoutKind.Explicit, Pack = 1)] internal struct GuidBits { [FieldOffset(0)] public readonly Guid Value; [FieldOffset(0)] public readonly byte Byte0; [FieldOffset(1)] public readonly byte Byte1; [FieldOffset(2)] public readonly byte Byte2; [FieldOffset(3)] public readonly byte Byte3; /* 中略(Byte4~Byte11) */ [FieldOffset(12)] public readonly byte Byte12; [FieldOffset(13)] public readonly byte Byte13; [FieldOffset(14)] public readonly byte Byte14; [FieldOffset(15)] public readonly byte Byte15; Guidとbyte0~16の重ね合わせ (同一FieldOffset) 通常Stringかbyte[]からしか生成でき ないGuidを、byte0~16を埋めるだけ で自由に生成する(本来弄れないGuid としてのメモリ領域を重ね合わせて 安全に(not unsafe)弄る) MessagePack for C#でUtf8 Bytesのス ライスから文字列のアロケーションを避 けて直接Guidに変換するのに利用
  59. 59. 改めてStructが要素の配列とは 60 — メモリにStructが単純に並んでいる X Y Z X Y Z X Y Z X Y ZVector3[] メモリをまるごとコピーするだけで 最速のシリアライズだよね説 (エンディアンは揃える) 実際MagicOnionで有効にすることが可 能(サーバーもC#なので直接メモリをぶ ん投げて受け取るのが簡単) ただしStructの中には参照型(String含 む)は含めないこと。ポイン タをコピー しても意味がない
  60. 60. 61 public class UnsafeDirectBlitArrayFormatter<T> : IMessagePackFormatter<T[]> where T : struct { public unsafe int Serialize(ref byte[] bytes, int offset, T[] value) { var startOffset = offset; var byteLen = value.Length * UnsafeUtility.SizeOf<T>(); /* 中略(MsgPackでのExtヘッダー書き込み) */ ulong handle2; var srcPointer = UnsafeUtility .PinGCArrayAndGetDataAddress(value, out handle2); try { fixed (void* dstPointer = &bytes[offset]) { UnsafeUtility.MemCpy(dstPointer, srcPointer, byteLen); } } finally { UnsafeUtility.ReleaseGCObject(handle2); } // ... } T[]をbytesにシリアライズ memcpyするだけ
  61. 61. 62 Span and NativeArray
  62. 62. Span vs NativeArray 63 — Span<T> — System.Memory — .NET Standard 2.1では標準(現在は外部ライブラリが必要) — C# 7.2と統合されている — あらゆる連続したメモリ領域のビュー — 配列、stackalloc、ネイティブメモリ(ポインタ)、文字列(String) — NativeArray — Unity固有, 特にDOTSのキーパーツ — UnsafeUtility.Malloc で獲得するUnmanaged Memoryのビュー
  63. 63. フレームワーク対応がないと意味がない 64 — Span<T> — 今までのAPIがT[]しか受け入れなかったりすると、結局T[]への変換が必要になる — 無駄アロケート — .NET Core 2.1で対応充実させ中 — 例えばConvert.ToBase64Stringがbyte[]のほかReadOnlySpan<byte>を受けとる — つまりUnityではAPI側の対応がほとんどないのでSpanだけ入れても意味は限定的 — NativeArray — DOTS周辺で使えるけれど、やはり一部のAPIは対応が必要 — 例えばMesh.SetVertexBufferDataとしてList<T>やT[]以外にNativeArray<T>を受け 取るようになったのは 2019.3から — というように、スムーズに全体的に統合されていくのはもうちょっとかな?
  64. 64. 65 Conclusion
  65. 65. の前に。 66 — 構造体の色をクラスとは別の色に変更しておこう! – 性能特性が異なるもののため、見分けがつくと、とても楽になる
  66. 66. Structを恐れない 67 — Structの使いこなし自体は、もはや必須 – Classを優先する牧歌的時代は終わった – 確かに罠は(いっぱい)あるが、難しい話ではない – 覚えるパターンが(ちょっと)多いだけで — そしてこの流れは戻らない – 言語強化から、フレームワークの抜本的変更まで、時代は既に来てる – ……とはいえ、じゃあいきなりめっちゃ使うかと言うと、それはまた別の話 – 使うべき時に使い、読めるようにするという、当たり前を大事にしよう

×