Understanding
C# Struct
All Things
Cysharp, Inc.
Kawai Yoshifumi
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#で実装する)を推進
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.
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.
6
The Evolution of C# Struct
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];
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
DOTS(昨日の基調講演より)
DOTS(昨日の基調講演より)
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
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
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
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
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表現
16
The Basic of C# Memory
The Memory of C#
17
AppDomain(Managed)
Thread
Stack
HeapThread
Stack
Unmanaged
The Memory of C#
18
AppDomain(Managed)
Thread
Stack
HeapThread
Stack
Unmanaged
ローカル変数はスタック領域に格納される
ヒープ領域に確保されたデータは
GCの管理化に入る
UnityではC#管理外のメモリを扱う
こともよくある(特にDOTS)
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)で中身が異なる
ことがある場合に注意
Struct Memory
Int(4バイト)のX, Y, Z(12バイト)が
素直にメモリ上(スタック)に並ぶ
Class Memory
スタック上の変数はヒープ上
のアドレスを指す
ヒープ上に確保されるメモリは管理
用のヘッダ/型情報 + 実データ
Pass by Reference/Pass by Value
22
— C#はデフォルトは全て「値渡し」
— つまりコピーされます
— ローカル変数への代入もコピー
(T x) (ref T x)
class 参照の値渡し 参照の参照渡し
struct 値の値渡し 値の参照渡し
スタック領域は大体この辺っぽい
00 00 00 00 からそれっぽ
いデータが入った気配
引き続き、 y = x で同じデ
ータが追加で入ったっぽい
別のところにxとyがコピー
されて入った気配
戻りのzを受け取って全部
埋まった
int x
int y
int 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
オブジェクトの共通ヘッダの後
ろに長さが付いてる
データ領域には要素がそのまま順番に並ぶ。構造体な
ら、値がそのまま順番に並んでいることになる(参照
型の場合はポインタが並んでいるため、実態のデータ
を更に辿る必要がある)
38
ref 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 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じゃなくてもいい
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];
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, 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はネイティブメモリを使ったり色々
と固有の事情があるので一般論は適用で
きない)
static void Normal(Vector3 v3)
{
}
static void In(in Vector3 v3)
{
}
static void Ref(ref Vector3 v3)
{
}
Avoid the copy, Everywhere
45
そこで登場するのが新しいキーワード
“in”
(このコード例自体には何の意味もないです)
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);
呼び側のコード
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
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 でいいね!
(にはならない)
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
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;
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
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のまま
55
Manipulate 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
.CompilerService
s.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があればそれでいい
それらはほとんどメモリコピーで実現する
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に変換するのに利用
改めてStructが要素の配列とは
60
— メモリにStructが単純に並んでいる
X Y Z X Y Z X Y Z X Y ZVector3[]
メモリをまるごとコピーするだけで
最速のシリアライズだよね説
(エンディアンは揃える)
実際MagicOnionで有効にすることが可
能(サーバーもC#なので直接メモリをぶ
ん投げて受け取るのが簡単)
ただしStructの中には参照型(String含
む)は含めないこと。ポイン タをコピー
しても意味がない
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するだけ
62
Span and NativeArray
Span vs NativeArray
63
— 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から
— というように、スムーズに全体的に統合されていくのはもうちょっとかな?
65
Conclusion
の前に。
66
— 構造体の色をクラスとは別の色に変更しておこう!
– 性能特性が異なるもののため、見分けがつくと、とても楽になる
Structを恐れない
67
— Structの使いこなし自体は、もはや必須
– Classを優先する牧歌的時代は終わった
– 確かに罠は(いっぱい)あるが、難しい話ではない
– 覚えるパターンが(ちょっと)多いだけで
— そしてこの流れは戻らない
– 言語強化から、フレームワークの抜本的変更まで、時代は既に来てる
– ……とはいえ、じゃあいきなりめっちゃ使うかと言うと、それはまた別の話
– 使うべき時に使い、読めるようにするという、当たり前を大事にしよう

【Unite Tokyo 2019】Understanding C# Struct All Things

  • 1.
  • 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.
    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.
    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.
  • 6.
  • 7.
    MessagePack for C#(v2-preview) 7 publicref 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];
  • 8.
    MessagePack for C#(v2-preview) 8 publicref 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
  • 9.
  • 10.
  • 11.
    What’s new structfeatures 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
  • 12.
    12 Which Unity Versionshould 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
  • 13.
    13 Which Unity Versionshould 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
  • 14.
    14 Which Unity Versionshould 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
  • 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 : 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表現
  • 16.
    16 The Basic ofC# Memory
  • 17.
    The Memory ofC# 17 AppDomain(Managed) Thread Stack HeapThread Stack Unmanaged
  • 18.
    The Memory ofC# 18 AppDomain(Managed) Thread Stack HeapThread Stack Unmanaged ローカル変数はスタック領域に格納される ヒープ領域に確保されたデータは GCの管理化に入る UnityではC#管理外のメモリを扱う こともよくある(特にDOTS)
  • 19.
    Let’s see memorylayout 19 — SharpLabでメモリの中身を見よう! — https://sharplab.io/ — C# to IL, C# to C#, C# to ASMなど豊富な機能がある — Run と Inspectを組み合わせるとメモリの中身が見れる なお、組み込み型の場合、Unity(mono) とSharpLab(.NET Core)で中身が異なる ことがある場合に注意
  • 20.
    Struct Memory Int(4バイト)のX, Y,Z(12バイト)が 素直にメモリ上(スタック)に並ぶ
  • 21.
  • 22.
    Pass by Reference/Passby Value 22 — C#はデフォルトは全て「値渡し」 — つまりコピーされます — ローカル変数への代入もコピー (T x) (ref T x) class 参照の値渡し 参照の参照渡し struct 値の値渡し 値の参照渡し
  • 23.
  • 24.
    00 00 0000 からそれっぽ いデータが入った気配
  • 25.
    引き続き、 y =x で同じデ ータが追加で入ったっぽい
  • 26.
  • 27.
  • 28.
    int x int y intz コンパイル時(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 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じゃなくてもいい
  • 41.
    ref struct 41 — スタックにしか置けないという制約が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,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はネイティブメモリを使ったり色々 と固有の事情があるので一般論は適用で きない)
  • 45.
    static void Normal(Vector3v3) { } static void In(in Vector3 v3) { } static void Ref(ref Vector3 v3) { } Avoid the copy, Everywhere 45 そこで登場するのが新しいキーワード “in” (このコード例自体には何の意味もないです)
  • 46.
    static void Normal(Vector3v3) { } static void In(in Vector3 v3) { } static void Ref(ref Vector3 v3) { } Avoid the copy, Everywhere 46 Normal(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, Everywhere 47 ldloc.0 call Noraml ldloca.0 call In ldloca.0 call Ref Normal(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, Everywhere 48 ldloc.0 call Noraml ldloca.0 call In ldloca.0 call 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, Everywhere 49 ldarga.0 call get_magnitude ldarg.0 ldobj stloc.0 ldloca.0 call get_magnitude ldarg.0 call 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, 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;
  • 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, 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
  • 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 .CompilerService s.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.
    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に変換するのに利用
  • 60.
    改めてStructが要素の配列とは 60 — メモリにStructが単純に並んでいる X YZ X Y Z X Y Z X Y ZVector3[] メモリをまるごとコピーするだけで 最速のシリアライズだよね説 (エンディアンは揃える) 実際MagicOnionで有効にすることが可 能(サーバーもC#なので直接メモリをぶ ん投げて受け取るのが簡単) ただしStructの中には参照型(String含 む)は含めないこと。ポイン タをコピー しても意味がない
  • 61.
    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するだけ
  • 62.
  • 63.
    Span vs NativeArray 63 —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を優先する牧歌的時代は終わった –確かに罠は(いっぱい)あるが、難しい話ではない – 覚えるパターンが(ちょっと)多いだけで — そしてこの流れは戻らない – 言語強化から、フレームワークの抜本的変更まで、時代は既に来てる – ……とはいえ、じゃあいきなりめっちゃ使うかと言うと、それはまた別の話 – 使うべき時に使い、読めるようにするという、当たり前を大事にしよう