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