Movatterモバイル変換


[0]ホーム

URL:


PDF, PPTX66,099 views

【Unite Tokyo 2019】Understanding C# Struct All Things

2019/9/25-6に開催されたUnite Tokyo 2019の講演スライドです。河合 宜文(株式会社Cysharp)こんな人におすすめ・C#を極めたいエンジニア・パフォーマンスに興味のあるエンジニア・プログラミング言語マニア受講者が得られる知見・structに関する深い知識・パフォーマンス向上のヒント・C#の新しい文法と活用法Unityのイベント資料はこちらから:https://www.slideshare.net/UnityTechnologiesJapan/clipboards

Embed presentation

Download as PDF, PPTX
UnderstandingC# StructAll ThingsCysharp, Inc.Kawai Yoshifumi
About Speaker2— 河合 宜文 / 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#で実装する)を推進
https://github.com/CysharpOSS for Unity – GitHub/Cysharp3UniTask ★288Provides an efficient async/await integration toUnity.RuntimeUnitTestToolkit ★87CLI/GUI Frontend of Unity Test Runner to test onany platforms.RandomFixtureKit ★16Fill random/edge-case value to target type forunit testing.MagicOnion ★1240Unified Realtime/API Engine for .NET Core andUnity.MasterMemory ★407Embedded Typed Readonly In-MemoryDocument Database for .NET Core and Unity.
https://github.com/neueccOSS for Unity – GitHub/neuecc4LINQ-to-GameObject-for-Unity ★448Traverse GameObject Hierarchy by LINQ.PhotonWire ★92Typed Asynchronous RPC Layer for Photon.SerializableDictionary ★87SerializableCollections for Unity.ReMotion ★27Hyper Fast Reactive Tween Engine for Unity.UniRx ★3722Reactive Extensions for Unity.MessagePack-CSharp ★2089Extremely Fast MessagePack Serializer.ZeroFormatter ★1778Infinitely Fast Deserializer.Utf8Json ★1352Definitely Fastest JSON Serializer.
6The Evolution of C# Struct
MessagePack for C#(v2-preview)7public ref struct MessagePackWriterT 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];
MessagePack for C#(v2-preview)8public ref struct MessagePackWriterT Deserialize<T>(in ReadOnlySequence<byte> byteSequence)Span<byte> bytes = stackalloc byte[36];internal ref partial struct SequenceReader<T>where T : unmanaged, IEquatable<T>ref structSpan<T> = stackallocin parameterwhere : unmanagedpublic ref byte GetPointer(int sizeHint)ref Entry v = ref entry[0];ref returnref local
DOTS(昨日の基調講演より)
DOTS(昨日の基調講演より)
What’s new struct features11C# 7.2 C# 7.3 C# 8.0in modifier on parameterref readonly modifier onmethod returns, localreadonly structdeclarationref struct declarationref extension methodstackalloc to Spanreassign ref localadditional genericsconstraints(unmanaged, Enum, Delegate)stackalloc initializeraccess fixed fieldwithout pinningreadonly methodDisposable ref structsUnmanagedconstructed typeshttps://docs.microsoft.com/en-us/dotnet/csharp/whats-new/C# 7.0ref return statementref local variables
12Which Unity Version should we support?Unity Version C# Version .NET VersionUnity 2017.4 6.0 .NET 3.5/.NET 4.6Unity 2018.2 6.0 .NET 4.x/Standard 2.0Unity 2018.3 7.3 .NET 4.x/Standard 2.0Unity 2018.4 7.3 .NET 4.x/Standard 2.0Unity 2019.1 7.3 .NET 4.x/Standard 2.0
13Which Unity Version should we support?Unity Version C# Version .NET VersionUnity 2017.4 6.0 .NET 3.5/.NET 4.6Unity 2018.2 6.0 .NET 4.x/Standard 2.0Unity 2018.3 7.3 .NET 4.x/Standard 2.0Unity 2018.4 7.3 .NET 4.x/Standard 2.0Unity 2019.1 7.3 .NET 4.x/Standard 2.02018.3以上一択、それ以下のバージョンはNot Supported
14Which Unity Version should we support?Unity Version C# Version .NET VersionUnity 2017.4 6.0 .NET 3.5/.NET 4.6Unity 2018.2 6.0 .NET 4.x/Standard 2.0Unity 2018.3 7.3 .NET 4.x/Standard 2.0Unity 2018.4 7.3 .NET 4.x/Standard 2.0Unity 2019.1 7.3 .NET 4.x/Standard 2.02018.3から以下の言語に関するdefineが使えるCSHARP_7_3_OR_NEWER以下のどちらか選んだほうが定義されるNET_4_6NET_STANDARD_2_0
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 : structpublic unsafe static ref T AsRef<T>(void* ptr) where T : structpublic readonly struct BuildComponentDataToEntityLookupTask<TComponentData> : IDisposablewhere TComponentData : unmanaged, IComponentData, IEquatable<TComponentData>Unity ECSの中のC# 7.3表現
16The Basic of C# Memory
The Memory of C#17AppDomain(Managed)ThreadStackHeapThreadStackUnmanaged
The Memory of C#18AppDomain(Managed)ThreadStackHeapThreadStackUnmanagedローカル変数はスタック領域に格納されるヒープ領域に確保されたデータはGCの管理化に入るUnityではC#管理外のメモリを扱うこともよくある(特にDOTS)
Let’s see memory layout19— SharpLabでメモリの中身を見よう!— https://sharplab.io/— C# to IL, C# to C#, C# to ASMなど豊富な機能がある— Run と Inspectを組み合わせるとメモリの中身が見れるなお、組み込み型の場合、Unity(mono)とSharpLab(.NET Core)で中身が異なることがある場合に注意
Struct MemoryInt(4バイト)のX, Y, Z(12バイト)が素直にメモリ上(スタック)に並ぶ
Class Memoryスタック上の変数はヒープ上のアドレスを指すヒープ上に確保されるメモリは管理用のヘッダ/型情報 + 実データ
Pass by Reference/Pass by Value22— C#はデフォルトは全て「値渡し」— つまりコピーされます— ローカル変数への代入もコピー(T x) (ref T x)class 参照の値渡し 参照の参照渡しstruct 値の値渡し 値の参照渡し
スタック領域は大体この辺っぽい
00 00 00 00 からそれっぽいデータが入った気配
引き続き、 y = x で同じデータが追加で入ったっぽい
別のところにxとyがコピーされて入った気配
戻りのzを受け取って全部埋まった
int xint yint zコンパイル時(C# -> IL)の段階で変数の置き場確保しておきます、的な
Structの基本的な原則29— 全てはコピーに気をつける、ということ— クラスとの違い、ほとんどの問題はコピーにより引き起こされる— 大きなサイズの構造体を(基本的には)作らない– IntPtr.Size(4 or 8バイト)以上は参照渡しに比べて大きいということになる– それだと制限キツすぎなので、一般には16バイト以下ぐらいを目安に— 変更可能な構造体を(基本的には)作らない– コピーされることによって、変更したつもりが変更されない– 一度は悩むVector3変更されない問題
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);
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++越境がどうしても抱える話)
Boxed Structボックス化スタック上の変数はヒープのアドレス ヒープ上にクラスと同様ヘッダが付いたうえで領域確保アンボックスの毎にここからスタックへコピーする
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;}ある程度決まった値が頻繁にボックス化されるようなら、先に作っておいて使い回すという技
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になるものだけ)でいいと思います
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オブジェクトの共通ヘッダの後ろに長さが付いてるデータ領域には要素がそのまま順番に並ぶ。構造体なら、値がそのまま順番に並んでいることになる(参照型の場合はポインタが並んでいるため、実態のデータを更に辿る必要がある)
38ref and readonly
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 */}
Mutable Struct is Evil but Useful40— 一時的な入れ物として使うものに向いてる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じゃなくてもいい
ref struct41— スタックにしか置けないという制約がref struct– 元々はSpan<T>(System.Memory, .NET Standard 2.0外部ライブラリ)のため– Span<T>は連続したメモリ領域のビューで、配列のように扱える(NativeArrayみたいな)– 今までポインタでしか扱えなかったstackallocを自然に扱えて便利– しかしそれによってスタックにのみ確保したメモリ領域をヒープに移されると危険– フィールドに置けない(ref structのfieldの場合のみ可)、ボクシングできない、インターフェイスを実装できない、ジェネリクスの型引数にできない、などの制約がある— ビュー的なものや一時的にしか使わない状態を持つものには適用しやすい– 制約が多いので無理に使おうとするとハマりますが……Span<int> temp = stackalloc int[12];
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
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から取ってるのでアロケートなしで済んだ
Avoid the copy, Everywhere44— 全て 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はネイティブメモリを使ったり色々と固有の事情があるので一般論は適用できない)
static void Normal(Vector3 v3){}static void In(in Vector3 v3){}static void Ref(ref Vector3 v3){}Avoid the copy, Everywhere45そこで登場するのが新しいキーワード“in”(このコード例自体には何の意味もないです)
static void Normal(Vector3 v3){}static void In(in Vector3 v3){}static void Ref(ref Vector3 v3){}Avoid the copy, Everywhere46Normal(v3);In(v3);Ref(ref v3);呼び側のコード
static void Normal(Vector3 v3){}static void In(in Vector3 v3){}static void Ref(ref Vector3 v3){}Avoid the copy, Everywhere47ldloc.0call Noramlldloca.0call Inldloca.0call RefNormal(v3);In(v3);Ref(ref v3);呼び側のIL
static void Normal(Vector3 v3){}static void In(in Vector3 v3){}static void Ref(ref Vector3 v3){}Avoid the copy, Everywhere48ldloc.0call Noramlldloca.0call Inldloca.0call Ref呼び方は普通と一緒なのにrefと同じく参照渡しされる!Normal(v3);In(v3);Ref(ref v3);じゃあ全部 in でいいね!(にはならない)
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, Everywhere49ldarga.0call get_magnitudeldarg.0ldobjstloc.0ldloca.0call get_magnitudeldarg.0call get_magnitude呼ばれ側のIL
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, Everywhere50ldarga.0call get_magnitudeldarg.0ldobjstloc.0ldloca.0call get_magnitudeldarg.0call get_magnitude呼ばれ側のILコピーされてる(防御的コピー)var v3_2 = v3;_ = v3_2.magnitude;
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, Everywhere51ldarga.0call get_magnitudeldarg.0ldobjstloc.0ldloca.0call get_magnitudeldarg.0call get_magnitude呼ばれ側のIL二回呼べばコピーもそのまま二つldarga.0call get_magnitudeldarg.0ldobjstloc.0ldloca.0call get_magnitudeldarg.0call get_magnitude
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は書き換わらないことが保証されるため防御的コピーが走らない
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中でフィールドしか絶対触らないと力強く言えるならナシではないよくわからないなら基本使わない
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のまま
55Manipulate Memory
改めてStructとは56— メモリを単純にマッピングした構造– そのことだけ意識すれば、あとはやりたい放題できる[StructLayout(LayoutKind.Sequential, Size = 16)]public struct Empty16{}public struct LongLong{public long X;public long Y;}どちらも連続して16バイトの領域を確保しているだけ、という意味(に読める)インデックス0とインデックス8にアクセスしやすくしているだけ(という風に読める)
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.CompilerServices.Unsafeを利用、Unityでも動きますが別途参照は必要、ポインタでやってもいいです)
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があればそれでいいそれらはほとんどメモリコピーで実現する
Union59[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に変換するのに利用
改めてStructが要素の配列とは60— メモリにStructが単純に並んでいるX Y Z X Y Z X Y Z X Y ZVector3[]メモリをまるごとコピーするだけで最速のシリアライズだよね説(エンディアンは揃える)実際MagicOnionで有効にすることが可能(サーバーもC#なので直接メモリをぶん投げて受け取るのが簡単)ただしStructの中には参照型(String含む)は含めないこと。ポイン タをコピーしても意味がない
61public 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するだけ
62Span and NativeArray
Span vs NativeArray63— Span<T>— System.Memory— .NET Standard 2.1では標準(現在は外部ライブラリが必要)— C# 7.2と統合されている— あらゆる連続したメモリ領域のビュー— 配列、stackalloc、ネイティブメモリ(ポインタ)、文字列(String)— NativeArray— Unity固有, 特にDOTSのキーパーツ— UnsafeUtility.Malloc で獲得するUnmanaged Memoryのビュー
フレームワーク対応がないと意味がない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から— というように、スムーズに全体的に統合されていくのはもうちょっとかな?
65Conclusion
の前に。66— 構造体の色をクラスとは別の色に変更しておこう!– 性能特性が異なるもののため、見分けがつくと、とても楽になる
Structを恐れない67— Structの使いこなし自体は、もはや必須– Classを優先する牧歌的時代は終わった– 確かに罠は(いっぱい)あるが、難しい話ではない– 覚えるパターンが(ちょっと)多いだけで— そしてこの流れは戻らない– 言語強化から、フレームワークの抜本的変更まで、時代は既に来てる– ……とはいえ、じゃあいきなりめっちゃ使うかと言うと、それはまた別の話– 使うべき時に使い、読めるようにするという、当たり前を大事にしよう

Recommended

PDF
CEDEC 2018 最速のC#の書き方 - C#大統一理論へ向けて性能的課題を払拭する
PDF
今日からできる!簡単 .NET 高速化 Tips
PPTX
C#で速度を極めるいろは
PDF
Unity開発で使える設計の話+Zenjectの紹介
PDF
Unityでオニオンアーキテクチャ
PDF
C++ マルチスレッドプログラミング
PPTX
最速C# 7.x
PDF
Unityではじめるオープンワールド制作 エンジニア編
PDF
コールバックと戦う話
PDF
Observable Everywhere - Rxの原則とUniRxにみるデータソースの見つけ方
PDF
こわくない Git
PDF
Unityでパフォーマンスの良いUIを作る為のTips
PDF
UE4でマルチプレイヤーゲームを作ろう
PDF
20分くらいでわかった気分になれるC++20コルーチン
 
PPTX
なぜなにリアルタイムレンダリング
PDF
基礎線形代数講座
PDF
Riderはいいぞ!
PDF
プログラムを高速化する話
PDF
テスト文字列に「うんこ」と入れるな
PDF
目grep入門 +解説
PDF
ゲーム開発者のための C++11/C++14
PPTX
C# 8.0 非同期ストリーム
PDF
【Unity道場スペシャル 2017札幌】最適化をする前に覚えておきたい技術 -札幌編-
PPTX
イベント駆動プログラミングとI/O多重化
PDF
Pythonによる黒魔術入門
PDF
不遇の標準ライブラリ - valarray
PDF
【Unite Tokyo 2019】Unity Test Runnerを活用して内部品質を向上しよう
PPTX
イベント・ソーシングを知る
PPTX
C#や.NET Frameworkがやっていること
PPTX
C# 9.0 / .NET 5.0

More Related Content

PDF
CEDEC 2018 最速のC#の書き方 - C#大統一理論へ向けて性能的課題を払拭する
PDF
今日からできる!簡単 .NET 高速化 Tips
PPTX
C#で速度を極めるいろは
PDF
Unity開発で使える設計の話+Zenjectの紹介
PDF
Unityでオニオンアーキテクチャ
PDF
C++ マルチスレッドプログラミング
PPTX
最速C# 7.x
PDF
Unityではじめるオープンワールド制作 エンジニア編
CEDEC 2018 最速のC#の書き方 - C#大統一理論へ向けて性能的課題を払拭する
今日からできる!簡単 .NET 高速化 Tips
C#で速度を極めるいろは
Unity開発で使える設計の話+Zenjectの紹介
Unityでオニオンアーキテクチャ
C++ マルチスレッドプログラミング
最速C# 7.x
Unityではじめるオープンワールド制作 エンジニア編

What's hot

PDF
コールバックと戦う話
PDF
Observable Everywhere - Rxの原則とUniRxにみるデータソースの見つけ方
PDF
こわくない Git
PDF
Unityでパフォーマンスの良いUIを作る為のTips
PDF
UE4でマルチプレイヤーゲームを作ろう
PDF
20分くらいでわかった気分になれるC++20コルーチン
 
PPTX
なぜなにリアルタイムレンダリング
PDF
基礎線形代数講座
PDF
Riderはいいぞ!
PDF
プログラムを高速化する話
PDF
テスト文字列に「うんこ」と入れるな
PDF
目grep入門 +解説
PDF
ゲーム開発者のための C++11/C++14
PPTX
C# 8.0 非同期ストリーム
PDF
【Unity道場スペシャル 2017札幌】最適化をする前に覚えておきたい技術 -札幌編-
PPTX
イベント駆動プログラミングとI/O多重化
PDF
Pythonによる黒魔術入門
PDF
不遇の標準ライブラリ - valarray
PDF
【Unite Tokyo 2019】Unity Test Runnerを活用して内部品質を向上しよう
PPTX
イベント・ソーシングを知る
コールバックと戦う話
Observable Everywhere - Rxの原則とUniRxにみるデータソースの見つけ方
こわくない Git
Unityでパフォーマンスの良いUIを作る為のTips
UE4でマルチプレイヤーゲームを作ろう
20分くらいでわかった気分になれるC++20コルーチン
 
なぜなにリアルタイムレンダリング
基礎線形代数講座
Riderはいいぞ!
プログラムを高速化する話
テスト文字列に「うんこ」と入れるな
目grep入門 +解説
ゲーム開発者のための C++11/C++14
C# 8.0 非同期ストリーム
【Unity道場スペシャル 2017札幌】最適化をする前に覚えておきたい技術 -札幌編-
イベント駆動プログラミングとI/O多重化
Pythonによる黒魔術入門
不遇の標準ライブラリ - valarray
【Unite Tokyo 2019】Unity Test Runnerを活用して内部品質を向上しよう
イベント・ソーシングを知る

Similar to 【Unite Tokyo 2019】Understanding C# Struct All Things

PPTX
C#や.NET Frameworkがやっていること
PPTX
C# 9.0 / .NET 5.0
PDF
C#の新機能勉強会 ~ C#7、8の新機能を活用して速く安全なプログラムを書こう~
PPTX
Deep Dive C# 6.0
PDF
C#勉強会 ~ C#9の新機能 ~
PDF
A quick tour of the Cysharp OSS
PPTX
.NET Compiler Platform
PDF
Inside FastEnum
PDF
UnityでC#を勉強しはじめた私の主張
PPTX
C# 8.0 Preview in Visual Studio 2019 (16.0)
PDF
ZeroFormatter/MagicOnion - Fastest C# Serializer/gRPC based C# RPC
PPTX
広がる .Net
PPTX
.NET Core 2.x 時代の C#
PPTX
C#6.0の新機能紹介
PPTX
C# design note sep 2014
PDF
What, Why, How Create OSS Libraries - 過去に制作した30のライブラリから見るC#コーディングテクニックと個人OSSの...
PPTX
.NET Core 3.0 で使える C# 8
PDF
【Unite Tokyo 2018 Training Day】C#JobSystem & ECSでCPUを極限まで使い倒そう ~Entity Compon...
PPTX
C#で最も使われていない言語機能はこれだ!
PPTX
C# 8
C#や.NET Frameworkがやっていること
C# 9.0 / .NET 5.0
C#の新機能勉強会 ~ C#7、8の新機能を活用して速く安全なプログラムを書こう~
Deep Dive C# 6.0
C#勉強会 ~ C#9の新機能 ~
A quick tour of the Cysharp OSS
.NET Compiler Platform
Inside FastEnum
UnityでC#を勉強しはじめた私の主張
C# 8.0 Preview in Visual Studio 2019 (16.0)
ZeroFormatter/MagicOnion - Fastest C# Serializer/gRPC based C# RPC
広がる .Net
.NET Core 2.x 時代の C#
C#6.0の新機能紹介
C# design note sep 2014
What, Why, How Create OSS Libraries - 過去に制作した30のライブラリから見るC#コーディングテクニックと個人OSSの...
.NET Core 3.0 で使える C# 8
【Unite Tokyo 2018 Training Day】C#JobSystem & ECSでCPUを極限まで使い倒そう ~Entity Compon...
C#で最も使われていない言語機能はこれだ!
C# 8

More from UnityTechnologiesJapan002

PDF
5分でわかる Sensor SDK
PDF
10分でわかる Unityコンピュータービジョン
PDF
5分でわかる Unity Forma
PDF
ROSのロボットモデルでバーチャルロボット受肉する
PDF
Unityでロボットの教師データは作れる!
PDF
ARとUnity-Robotics-Hubの連携
PDF
産業用ロボット開発におけるUnityの活用
PDF
建設シミュレータOCSの開発 / OCS・VTC on Unity におけるROS対応機能について
PDF
UnityとROSの連携について
PDF
中国深センから盛り上がる、ソフトウェアフレンドリーなロボティクス
PDF
Unityでお手軽ロボット開発「toio SDK for Unity」最新事例
PDF
集まれ!Dreamingエンジニア! 〜箱庭で紡ぎ出されるIoT/クラウドロボティクス開発の新しいカタチ〜
PDF
5分でわかる Unity点群
PDF
5分でわかる Unity Reflect
PDF
BIMからはじまる異世界転生 ~Unity Reflect が叶える新しい建築の世界~
PDF
【Unity道場 自動車編】Unityで実現する産業向けxRソリューション
PDF
【Unity道場 自動車編】トヨタのxR活用で進める現場DXへの挑戦 ~UnityとHoloLens 2を用いて~
PPTX
【Unity道場 自動車編】空間再現ディスプレイの概要と活用事例
PDF
【Unity道場 自動車編】 リアルタイム3D技術が支えるデジタルツイン
PPTX
【Unity道場 自動車編】モビリティへの活用に向けて
5分でわかる Sensor SDK
10分でわかる Unityコンピュータービジョン
5分でわかる Unity Forma
ROSのロボットモデルでバーチャルロボット受肉する
Unityでロボットの教師データは作れる!
ARとUnity-Robotics-Hubの連携
産業用ロボット開発におけるUnityの活用
建設シミュレータOCSの開発 / OCS・VTC on Unity におけるROS対応機能について
UnityとROSの連携について
中国深センから盛り上がる、ソフトウェアフレンドリーなロボティクス
Unityでお手軽ロボット開発「toio SDK for Unity」最新事例
集まれ!Dreamingエンジニア! 〜箱庭で紡ぎ出されるIoT/クラウドロボティクス開発の新しいカタチ〜
5分でわかる Unity点群
5分でわかる Unity Reflect
BIMからはじまる異世界転生 ~Unity Reflect が叶える新しい建築の世界~
【Unity道場 自動車編】Unityで実現する産業向けxRソリューション
【Unity道場 自動車編】トヨタのxR活用で進める現場DXへの挑戦 ~UnityとHoloLens 2を用いて~
【Unity道場 自動車編】空間再現ディスプレイの概要と活用事例
【Unity道場 自動車編】 リアルタイム3D技術が支えるデジタルツイン
【Unity道場 自動車編】モビリティへの活用に向けて

【Unite Tokyo 2019】Understanding C# Struct All Things

  • 1.
  • 2.
    About Speaker2— 河合宜文 / 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.
    https://github.com/CysharpOSS for Unity– GitHub/Cysharp3UniTask ★288Provides an efficient async/await integration toUnity.RuntimeUnitTestToolkit ★87CLI/GUI Frontend of Unity Test Runner to test onany platforms.RandomFixtureKit ★16Fill random/edge-case value to target type forunit testing.MagicOnion ★1240Unified Realtime/API Engine for .NET Core andUnity.MasterMemory ★407Embedded Typed Readonly In-MemoryDocument Database for .NET Core and Unity.
  • 4.
    https://github.com/neueccOSS for Unity– GitHub/neuecc4LINQ-to-GameObject-for-Unity ★448Traverse GameObject Hierarchy by LINQ.PhotonWire ★92Typed Asynchronous RPC Layer for Photon.SerializableDictionary ★87SerializableCollections for Unity.ReMotion ★27Hyper Fast Reactive Tween Engine for Unity.UniRx ★3722Reactive Extensions for Unity.MessagePack-CSharp ★2089Extremely Fast MessagePack Serializer.ZeroFormatter ★1778Infinitely Fast Deserializer.Utf8Json ★1352Definitely Fastest JSON Serializer.
  • 6.
  • 7.
    MessagePack for C#(v2-preview)7publicref struct MessagePackWriterT 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];
  • 8.
    MessagePack for C#(v2-preview)8publicref struct MessagePackWriterT Deserialize<T>(in ReadOnlySequence<byte> byteSequence)Span<byte> bytes = stackalloc byte[36];internal ref partial struct SequenceReader<T>where T : unmanaged, IEquatable<T>ref structSpan<T> = stackallocin parameterwhere : unmanagedpublic ref byte GetPointer(int sizeHint)ref Entry v = ref entry[0];ref returnref local
  • 9.
  • 10.
  • 11.
    What’s new structfeatures11C# 7.2 C# 7.3 C# 8.0in modifier on parameterref readonly modifier onmethod returns, localreadonly structdeclarationref struct declarationref extension methodstackalloc to Spanreassign ref localadditional genericsconstraints(unmanaged, Enum, Delegate)stackalloc initializeraccess fixed fieldwithout pinningreadonly methodDisposable ref structsUnmanagedconstructed typeshttps://docs.microsoft.com/en-us/dotnet/csharp/whats-new/C# 7.0ref return statementref local variables
  • 12.
    12Which Unity Versionshould we support?Unity Version C# Version .NET VersionUnity 2017.4 6.0 .NET 3.5/.NET 4.6Unity 2018.2 6.0 .NET 4.x/Standard 2.0Unity 2018.3 7.3 .NET 4.x/Standard 2.0Unity 2018.4 7.3 .NET 4.x/Standard 2.0Unity 2019.1 7.3 .NET 4.x/Standard 2.0
  • 13.
    13Which Unity Versionshould we support?Unity Version C# Version .NET VersionUnity 2017.4 6.0 .NET 3.5/.NET 4.6Unity 2018.2 6.0 .NET 4.x/Standard 2.0Unity 2018.3 7.3 .NET 4.x/Standard 2.0Unity 2018.4 7.3 .NET 4.x/Standard 2.0Unity 2019.1 7.3 .NET 4.x/Standard 2.02018.3以上一択、それ以下のバージョンはNot Supported
  • 14.
    14Which Unity Versionshould we support?Unity Version C# Version .NET VersionUnity 2017.4 6.0 .NET 3.5/.NET 4.6Unity 2018.2 6.0 .NET 4.x/Standard 2.0Unity 2018.3 7.3 .NET 4.x/Standard 2.0Unity 2018.4 7.3 .NET 4.x/Standard 2.0Unity 2019.1 7.3 .NET 4.x/Standard 2.02018.3から以下の言語に関するdefineが使えるCSHARP_7_3_OR_NEWER以下のどちらか選んだほうが定義されるNET_4_6NET_STANDARD_2_0
  • 15.
    Struct is importantfor Performance!15— C# 7以降の急速なstruct強化はパフォーマンスのため— それは .NET Core でも、Unityでも— アプローチは異なれど、両者とも構造体をパフォーマンスのため活用している— 特にUnityの推すDOTS(Data Oriented Technology Stack)はstructの塊— 今、全てを学び、備えようpublic unsafe ref struct BlobBuilderArray<T>where T : structpublic unsafe static ref T AsRef<T>(void* ptr) where T : structpublic readonly struct BuildComponentDataToEntityLookupTask<TComponentData> : IDisposablewhere TComponentData : unmanaged, IComponentData, IEquatable<TComponentData>Unity ECSの中のC# 7.3表現
  • 16.
  • 17.
    The Memory ofC#17AppDomain(Managed)ThreadStackHeapThreadStackUnmanaged
  • 18.
    The Memory ofC#18AppDomain(Managed)ThreadStackHeapThreadStackUnmanagedローカル変数はスタック領域に格納されるヒープ領域に確保されたデータはGCの管理化に入るUnityではC#管理外のメモリを扱うこともよくある(特にDOTS)
  • 19.
    Let’s see memorylayout19— SharpLabでメモリの中身を見よう!— https://sharplab.io/— C# to IL, C# to C#, C# to ASMなど豊富な機能がある— Run と Inspectを組み合わせるとメモリの中身が見れるなお、組み込み型の場合、Unity(mono)とSharpLab(.NET Core)で中身が異なることがある場合に注意
  • 20.
    Struct MemoryInt(4バイト)のX, Y,Z(12バイト)が素直にメモリ上(スタック)に並ぶ
  • 21.
  • 22.
    Pass by Reference/Passby Value22— C#はデフォルトは全て「値渡し」— つまりコピーされます— ローカル変数への代入もコピー(T x) (ref T x)class 参照の値渡し 参照の参照渡しstruct 値の値渡し 値の参照渡し
  • 23.
  • 24.
    00 00 0000 からそれっぽいデータが入った気配
  • 25.
    引き続き、 y =x で同じデータが追加で入ったっぽい
  • 26.
  • 27.
  • 28.
    int xint yintzコンパイル時(C# -> IL)の段階で変数の置き場確保しておきます、的な
  • 29.
    Structの基本的な原則29— 全てはコピーに気をつける、ということ— クラスとの違い、ほとんどの問題はコピーにより引き起こされる—大きなサイズの構造体を(基本的には)作らない– IntPtr.Size(4 or 8バイト)以上は参照渡しに比べて大きいということになる– それだと制限キツすぎなので、一般には16バイト以下ぐらいを目安に— 変更可能な構造体を(基本的には)作らない– コピーされることによって、変更したつもりが変更されない– 一度は悩むVector3変更されない問題
  • 30.
    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);
  • 31.
    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++越境がどうしても抱える話)
  • 32.
  • 33.
    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;}ある程度決まった値が頻繁にボックス化されるようなら、先に作っておいて使い回すという技
  • 34.
    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になるものだけ)でいいと思います
  • 35.
    Struct Layout andPadding単純計算ではbyte(1) + long(8) + int(4) = 13ですが、アラインメント調整のため、最長の8にそれぞれが合わせられて8 * 3 = 24バイトの確保になっている
  • 36.
    Struct Layout andPadding StructLayoutやFieldOffsetによってレイアウトはカスタマイズ可能。structのデフォルトはSequential(宣言順)Autoに変えると、ZとXが詰められることで最小の16バイトに縮む参照型はstructと異なりデフォルトがAuto
  • 37.
  • 38.
  • 39.
    Zero Allocation foreachin 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 */}
  • 40.
    Mutable Struct isEvil but Useful40— 一時的な入れ物として使うものに向いてる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じゃなくてもいい
  • 41.
    ref struct41— スタックにしか置けないという制約がrefstruct– 元々はSpan<T>(System.Memory, .NET Standard 2.0外部ライブラリ)のため– Span<T>は連続したメモリ領域のビューで、配列のように扱える(NativeArrayみたいな)– 今までポインタでしか扱えなかったstackallocを自然に扱えて便利– しかしそれによってスタックにのみ確保したメモリ領域をヒープに移されると危険– フィールドに置けない(ref structのfieldの場合のみ可)、ボクシングできない、インターフェイスを実装できない、ジェネリクスの型引数にできない、などの制約がある— ビュー的なものや一時的にしか使わない状態を持つものには適用しやすい– 制約が多いので無理に使おうとするとハマりますが……Span<int> temp = stackalloc int[12];
  • 42.
    internal ref structTempList<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
  • 43.
    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から取ってるのでアロケートなしで済んだ
  • 44.
    Avoid the copy,Everywhere44— 全て 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はネイティブメモリを使ったり色々と固有の事情があるので一般論は適用できない)
  • 45.
    static void Normal(Vector3v3){}static void In(in Vector3 v3){}static void Ref(ref Vector3 v3){}Avoid the copy, Everywhere45そこで登場するのが新しいキーワード“in”(このコード例自体には何の意味もないです)
  • 46.
    static void Normal(Vector3v3){}static void In(in Vector3 v3){}static void Ref(ref Vector3 v3){}Avoid the copy, Everywhere46Normal(v3);In(v3);Ref(ref v3);呼び側のコード
  • 47.
    static void Normal(Vector3v3){}static void In(in Vector3 v3){}static void Ref(ref Vector3 v3){}Avoid the copy, Everywhere47ldloc.0call Noramlldloca.0call Inldloca.0call RefNormal(v3);In(v3);Ref(ref v3);呼び側のIL
  • 48.
    static void Normal(Vector3v3){}static void In(in Vector3 v3){}static void Ref(ref Vector3 v3){}Avoid the copy, Everywhere48ldloc.0call Noramlldloca.0call Inldloca.0call Ref呼び方は普通と一緒なのにrefと同じく参照渡しされる!Normal(v3);In(v3);Ref(ref v3);じゃあ全部 in でいいね!(にはならない)
  • 49.
    static void Normal(Vector3v3){_ = v3.magnitude;}static void In(in Vector3 v3){_ = v3.magnitude;}static void Ref(ref Vector3 v3){_ = v3.magnitude;}Avoid the copy, Everywhere49ldarga.0call get_magnitudeldarg.0ldobjstloc.0ldloca.0call get_magnitudeldarg.0call get_magnitude呼ばれ側のIL
  • 50.
    static void Normal(Vector3v3){_ = v3.magnitude;}static void In(in Vector3 v3){_ = v3.magnitude;}static void Ref(ref Vector3 v3){_ = v3.magnitude;}Avoid the copy, Everywhere50ldarga.0call get_magnitudeldarg.0ldobjstloc.0ldloca.0call get_magnitudeldarg.0call get_magnitude呼ばれ側のILコピーされてる(防御的コピー)var v3_2 = v3;_ = v3_2.magnitude;
  • 51.
    static void Normal(Vector3v3){_ = 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, Everywhere51ldarga.0call get_magnitudeldarg.0ldobjstloc.0ldloca.0call get_magnitudeldarg.0call get_magnitude呼ばれ側のIL二回呼べばコピーもそのまま二つldarga.0call get_magnitudeldarg.0ldobjstloc.0ldloca.0call get_magnitudeldarg.0call get_magnitude
  • 52.
    Best practice touse `in`52— in はコンパイルすると([In][IsReadOnly]ref T t) になる— 読み取り専用のため、フィールドへの代入はできない— v3.x = 10.0f; // compile error— メソッド呼び出しは可能だが、中身が変わらない保証がないため防御的コピーが走る— そのため、うかつに多用すると防御的コピーにより、むしろ性能低下もありうる— もしそれがMutable Structだともはやわけわからないことに— (Vector3.magnitudeはプロパティなのでメソッド扱い)— 防御的コピーが走らない条件は– プロパティ/メソッドを呼ばないこと– あるいは readonly struct であること– readonly structは書き換わらないことが保証されるため防御的コピーが走らない
  • 53.
    Best practice touse `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中でフィールドしか絶対触らないと力強く言えるならナシではないよくわからないなら基本使わない
  • 54.
    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のまま
  • 55.
  • 56.
    改めてStructとは56— メモリを単純にマッピングした構造– そのことだけ意識すれば、あとはやりたい放題できる[StructLayout(LayoutKind.Sequential,Size = 16)]public struct Empty16{}public struct LongLong{public long X;public long Y;}どちらも連続して16バイトの領域を確保しているだけ、という意味(に読める)インデックス0とインデックス8にアクセスしやすくしているだけ(という風に読める)
  • 57.
    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.CompilerServices.Unsafeを利用、Unityでも動きますが別途参照は必要、ポインタでやってもいいです)
  • 58.
    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があればそれでいいそれらはほとんどメモリコピーで実現する
  • 59.
    Union59[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に変換するのに利用
  • 60.
    改めてStructが要素の配列とは60— メモリにStructが単純に並んでいるX YZ X Y Z X Y Z X Y ZVector3[]メモリをまるごとコピーするだけで最速のシリアライズだよね説(エンディアンは揃える)実際MagicOnionで有効にすることが可能(サーバーもC#なので直接メモリをぶん投げて受け取るのが簡単)ただしStructの中には参照型(String含む)は含めないこと。ポイン タをコピーしても意味がない
  • 61.
    61public 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するだけ
  • 62.
  • 63.
    Span vs NativeArray63—Span<T>— System.Memory— .NET Standard 2.1では標準(現在は外部ライブラリが必要)— C# 7.2と統合されている— あらゆる連続したメモリ領域のビュー— 配列、stackalloc、ネイティブメモリ(ポインタ)、文字列(String)— NativeArray— Unity固有, 特にDOTSのキーパーツ— UnsafeUtility.Malloc で獲得するUnmanaged Memoryのビュー
  • 64.
    フレームワーク対応がないと意味がない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から— というように、スムーズに全体的に統合されていくのはもうちょっとかな?
  • 65.
  • 66.
  • 67.
    Structを恐れない67— Structの使いこなし自体は、もはや必須– Classを優先する牧歌的時代は終わった–確かに罠は(いっぱい)あるが、難しい話ではない– 覚えるパターンが(ちょっと)多いだけで— そしてこの流れは戻らない– 言語強化から、フレームワークの抜本的変更まで、時代は既に来てる– ……とはいえ、じゃあいきなりめっちゃ使うかと言うと、それはまた別の話– 使うべき時に使い、読めるようにするという、当たり前を大事にしよう

[8]ページ先頭

©2009-2025 Movatter.jp