6.4K Views
August 22, 18
スライド概要
2018/8/22に開催されたCEDEC2018の講演資料です。
講師:安原 祐二(ユニティ・テクノロジーズ・ジャパン合同会社)
リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。
CPUを使い切れ! Entity Component System(通称ECS) が切り開く新しいプログラミング ユニティ・テクノロジーズ・ジャパン合同会社 フィールドエンジニア 安原 祐二
おすすめ資料 ハードウェアの性能を活かす為の、Unityの新しい3つの機能 動画: https://www.youtube.com/watch?v=eA2t8HBtRzg スライド資料: https://www.slideshare.net/UnityTechnologiesJapan/gtmf2018tokyounity3
{
"dependencies": {
本講演の対象バージョン
Unity2018.2.2f1
"com.unity.entities": "0.0.12-preview.8",
"com.unity.package-manager-ui": "2.0.0-preview.3",
"com.unity.modules.ai": "1.0.0",
"com.unity.modules.animation": "1.0.0",
"com.unity.modules.assetbundle": "1.0.0",
"com.unity.modules.audio": "1.0.0",
"com.unity.modules.cloth": "1.0.0",
"com.unity.modules.director": "1.0.0",
"com.unity.modules.imageconversion": "1.0.0",
"com.unity.modules.imgui": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.particlesystem": "1.0.0",
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.physics2d": "1.0.0",
"com.unity.modules.screencapture": "1.0.0",
"com.unity.modules.terrain": "1.0.0",
"com.unity.modules.terrainphysics": "1.0.0",
Packages/manifest.json→
"com.unity.modules.tilemap": "1.0.0",
"com.unity.modules.ui": "1.0.0",
"com.unity.modules.uielements": "1.0.0",
"com.unity.modules.umbra": "1.0.0",
"com.unity.modules.unityanalytics": "1.0.0",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.unitywebrequestassetbundle": "1.0.0",
"com.unity.modules.unitywebrequestaudio": "1.0.0",
"com.unity.modules.unitywebrequesttexture": "1.0.0",
"com.unity.modules.unitywebrequestwww": "1.0.0",
"com.unity.modules.vehicles": "1.0.0",
"com.unity.modules.video": "1.0.0",
"com.unity.modules.vr": "1.0.0",
"com.unity.modules.wind": "1.0.0",
"com.unity.modules.xr": "1.0.0"
},
"registry": "https://packages.unity.com",
"testables": [
"com.unity.collections",
"com.unity.entities",
"com.unity.jobs"
]
}
本講演のプロジェクト https://github.com/Unity-Technologies/AnotherThreadECS デモ動画 https://youtu.be/nOeOrJRf5NY
本日のコース 1. 復習 class と struct 11.デモ 2. Nativeコンテナってなんだ 12.JobだってEntityを生成したい 3. C# Job System 概要 13.Entityを追いかけろ! 4. CPUキャッシュのおさらい 14.実装! 5. ECSの思想に迫る 15.Jobテクをもうひとつだけ 6. Entity生成 16.コリジョン作ってみた step by step トレイルレンダリング 7. ザ・Burst 17.自作せよ! 8. マルチスレッドのおさらい 18.選ぶのは8個 9. 最重要! 19.伝えたい ComponentDataArray 10.IComponentDataを見極める Nativeコンテナ Entity間通信 20.ECS & Job が奏でる未来
“ 1.復習 class と struct ”
1.復習 class と struct class=参照型 サイズ不定 (継承、文字列型…) struct=値型 サイズ固定 (int、Vector3…)
1.復習 class と struct 配列を考える 0 1 2 そもそも配列は 等間隔が必須 3 4 5 6 7 参照型はサイズが異なるため 配列に格納できない
1.復習 class と struct 参照型の配列 0 1 2 ポインタは等間隔 3 4 5 6 7 サイズの異なる実体を別の場所に置く メモリはバラバラ
class と struct 値型の配列 0 1 2 3 4 5 6 7 サイズ一定 1.復習 メモリは カタマリになる
“ 2.Nativeコンテナってなんだ ”
2.Nativeコンテナってなんだ Unmanaged(GCが起きない)メモリを使用したコンテナ • NativeArray • 通常配列 • ポインタからの変換可能(ConvertExistingDataToNativeArray) • NativeSlice • NativeArrayの部分切り出しが可能 • ポインタからの変換可能(ConvertExistingDataToNativeSlice) • NativeList • マルチスレッド書き込み非対応 • NativeArray化にメモリコピーを使用しない • Job中でのサイズ変更後の状態でNativeArray化できる ToDeferredJobArray • NativeQueue • いわゆるFIFO。マルチスレッド書き込み対応 • NativeHashmap, NativeMultiHashmap • Key-Value コンテナ。マルチスレッド書き込み対応
2.Nativeコンテナってなんだ 例:NativeArray 生成 var a = new NativeArray<MyStruct>(32, Allocator.Persistent); 解放 a.Dispose(); Allocator.Persistent 永続的に使用可能 Allocator.Temp 同じフレームで解放しないとエラー Allocator.TempJob 4フレーム以内に解放しないとエラー フレームの概念が組み込まれている素晴らしさ
2.Nativeコンテナってなんだ とはいえ通常の配列を要求するUnityのAPIは多い・・・ 生成 var a = new NativeArray<MyStruct>(32, Allocator.Persistent); var b = new MyStruct[32]; コピー (memcpyのloop) a.CopyTo(b); a.CopyFrom(b); 今後NativeArray対応APIは増えていきます
2.Nativeコンテナってなんだ Nativeコンテナまとめ • Unityが用意した便利コンテナ • Managedメモリを使用しない • Gabage Collection とは無縁 • 実装はMalloc/Freeを使用してメモリ確保 • structで定義されているが概念は参照型(スマートポインタ)に近い • IL2CPPを想定した効率化が計られている • 性能測定はビルド後のものを • ユーザの自作も設計に組み込まれている(後述)
“ 3.C# Job System 概要 ”
3.C# Job System 概要 使いやすくなったThread ・これまでもスレッドは自作できた var th = new System.Threading.Thread(Func); th.Start(); ・Unityが用意したWorkerThreadを使えるように プロファイラで可視化 安全に書ける(エラーがたくさん出てくれる)
3.C# Job System 概要 Worker Thread で動いている
3.C# Job System 概要 WorkerThreadのメリット ・メニーコアの有効利用 ・Main Thread 動作中に実行可能 Camera.Renderなどの不可避な処理の裏はたいてい空いている
3.C# Job System のおさらい Camera.Renderの裏で動いている Camera.Render
3.C# Job System のおさらい
Jobの書きかた
Uniform的なもの
struct AJob : IJobParallelFor {
public NativeArray<Vector3> positions;
public void Execute(int i) {
var pos = positions[i];
Jobが実行する
indexは供給される
pos.y += 1f;
positions[i] = pos;
}
}
IJobParallelForを実装してExecuteを定義
3.C# Job System のおさらい
Jobの呼びかた
生成
void Update() {
データ参照
struct AJob : IJobParallelFor {
public NativeArray<Vector3> positions;
public void Execute(int id) {
var pos = positions[id];
pos.y += 1f;
positions[id] = pos;
}
}
var ajob = new AJob() { positions = m_Positions, };
var handle = ajob.Schedule(positions.Length, 8, );
}
実行指令
毎フレームScheduleを呼ぶ
読んでしまったら ajob は破棄して良い
3.C# Job System のおさらい
Jobの書きかた
Uniform的なもの
struct AJob : IJobParallelFor {
public NativeArray<Vector3> positions;
public void Execute(int id) {
var pos = positions[id];
idは供給される
pos.y += 1f;
Jobが実行する
positions[id] = pos;
}
}
参照なので危険がつきまとう
[ReadOnly]や[WriteOnly]属性をつける
3.C# Job System のおさらい 出してくれるエラーの例 InvalidOperationException: The native container has been declared as [WriteOnly] in the job, but you are reading from it. Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.Check ReadAndThrowNoEarlyOut (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) <0x1472e5a80 + 0x00052> 本当にありがとうございます
3.C# Job System のおさらい ジョブ発行時に依存関係を定義 void Update() { var ajob = new AJob() { positions = m_Positions, }; var bjob = new BJob() { positions = m_Positions, }; var handle = ajob.Schedule(positions.Length, 8, ); handle = bjob.Schedule(handle); JobHandle.ScheduleBatchedJobs(); handle.Complete(); } Scheduleの引数にJobHandleを渡す
3.C# Job System のおさらい C# Job Systemまとめ • 危険なマルチスレッドを回避 • 属性[ReadOnly][WriteOnly]でランタイムチェック • 依存や同期が簡単に書ける • 命令の発行(Schedule)や同期(Complete)がメインスレッドからしか呼べない • デッドロックを起こせない
“ 4.CPUキャッシュのおさらい ”
4.CPUキャッシュのおさらい メモリ・キャッシュ・メニーコア メモリ 遅い L2キャッシュ L1 L1 L1 L1 L1 L1 L1 L1 コア コア コア コア コア コア コア コア 速い
4.CPUキャッシュのおさらい メモリ・キャッシュ・メニーコア メモリ 遅い x20 L2キャッシュ x200 L1 L1 L1 L1 L1 L1 L1 L1 コア コア コア コア コア コア コア コア x10 速い (※数値は例)
4.CPUキャッシュのおさらい メインメモリ キャッシュラインは64bytes L2キャッシュ 64 1byteの使用でも 近傍の64bytesが キャッシュに乗る 64 L1 配置を最適化すれば 100倍ぐらい速いはず(?) コア コ
“ 5.ECSの思想に迫る ”
5.ECSの思想に迫る 横の要素をメモリ的に近くに置く Position Position Position Position Position Position Position Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Health Rigidbody Rigidbody Rigidbody Position Position Position Rigidbody Rigidbody Rigidbody Rotation Rotation Rotation Transform Transform Transform Transform Transform Transform Transform Matrix Matrix Matrix Matrix Matrix Matrix Matrix Renderer Renderer Renderer Enemy Enemy Renderer Renderer Bullet Bullet Renderer Renderer Missile Missile Player
5.ECSの思想に迫る Entity(物体) Position Position Position Position Position Position Position Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Health Rigidbody Rigidbody Rigidbody Position Position Position Rigidbody Rigidbody Rigidbody Rotation Rotation Rotation Transform Transform Transform Transform Transform Transform Transform Matrix Matrix Matrix Matrix Matrix Matrix Matrix Renderer Renderer Renderer Enemy Enemy Renderer Renderer Bullet Bullet Renderer Renderer Missile Missile Player
5.ECSの思想に迫る ComponentData(要素) Position Position Position Position Position Position Position Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Health Rigidbody Rigidbody Rigidbody Position Position Position Rigidbody Rigidbody Rigidbody Rotation Rotation Rotation Transform Transform Transform Transform Transform Transform Transform Matrix Matrix Matrix Matrix Matrix Matrix Matrix Renderer Renderer Renderer Enemy Enemy Renderer Renderer Bullet Bullet Renderer Renderer Missile Missile Player
5.ECSの思想に迫る System(共通部分に対する処理) Position Position Position Position Position Position Position Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Health Rigidbody Rigidbody Rigidbody Position Position Position Rigidbody Rigidbody Rigidbody Rotation Rotation Rotation Transform Transform Transform Transform Transform Transform Transform Matrix Matrix Matrix Matrix Matrix Matrix Matrix Renderer Renderer Renderer Enemy Enemy Renderer Renderer Bullet Bullet Renderer Renderer Missile Missile Player
5.ECSの思想に迫る 例:RigidbodyPositionSystemが対象とするデータ Position Position Position Position Position Position Position Rotation Rotation Rotation Rotation Rotation Rotation Rotation Health Health Health Health Health Rigidbody Rigidbody Rigidbody Position Position Position Rigidbody Rigidbody Rigidbody Rotation Rotation Rotation Transform Transform Transform Transform Transform Transform Transform Matrix Matrix Matrix Matrix Matrix Matrix Matrix Renderer Renderer Renderer Enemy Enemy Renderer Renderer Bullet Bullet Renderer Renderer Missile Missile Player
5.ECSの思想に迫る ECSまとめ • メモリ配置を考慮 • ECSに従えば効率の良いデータ配置を実現できる • System で同一要素を一括処理
“ 6.Entity生成 step by step ”
6.Entity生成 step by step 手順1/3・Componentの定義 public struct RigidbodyPosition : IComponentData { public float3 velocity; public float3 acceleration; public float damper; } IComponentData は interface
6.Entity生成 step by step …/[email protected]/Unity.Entities/IComponentData.cs namespace Unity.Entities { public interface IComponentData { } 何も書いてない
6.Entity生成 step by step 例:Positionの定義 …/[email protected]/Unity.Transforms/PositionComponent.cs namespace Unity.Transforms { public struct Position : IComponentData { public float3 Value; } } ところで、なにか違和感を感じませんか?
6.Entity生成 step by step 構造体が型ではなく、具体的な意味になる 通常: struct Vector3 様々な意味に使う Vector3 position; Vector3 velocity; Vector3 aimDirection; ECS: struct Position 構造体が特定の意味 struct Velocity struct AimDirection
6.Entity生成 step by step 手順2/3・ArchType(型)を作る var entity_manager = World.Active.GetOrCreateManager<EntityManager>(); arche_type = entity_manager.CreateArchetype(typeof(Unity.Transforms.Position) , typeof(Unity.Transforms.Rotation) , typeof(Unity.Transforms.TransformMatrix) , typeof(RigidbodyPosition) , typeof(RigidbodyRotation) , typeof(SphereCollider) , typeof(HitInfoPlayer) , typeof(Player) , typeof(Unity.Rendering.MeshInstanceRenderer)); ふつうは起動時にやっておく
6.Entity生成
step by step
手順3/3・CreateEntityでEntityを作成
var entity = entity_manager.CreateEntity(arche_type);
SetComponentDataで初期化
var entity = entity_manager.CreateEntity(arche_type);
var pos0 = new Unity.Transforms.Position { Value = new float3(0,0,0), };
entity_manager.SetComponentData<Unity.Transforms.Position>(entity, pos0);
SetComponentDataがEntityManager経由!
オブジェクト指向との思想の違い
6.Entity生成 step by step Entity生成まとめ • コンパイル時にComponentDataを定義 • 実行時にArchetypeを定義 • ArchetypeからEntityを生成 • Entityは単なるIDでしかない • すべてを把握するEntityManager経由で操作する
“ 7.ザ・Burst ”
7.ザ・Burst
高速コード(主にSIMD化による)を生成する特殊コンパイラ
[BurstCompile]
struct AJob : IJobParallelFor {
public NativeArray<Vector3> positions;
public void Execute(int id) {
var pos = positions[id];
pos.y += 1f;
ここがBurstCompileされる
positions[id] = pos;
}
}
何もかも速くなるわけではない
7.ザ・Burst いつコンパイルしているのか? タイミングをコンソールに出すと・・ なんと実行時 エディタ実行の場合
7.ザ・Burst 差し替えられるということは 副作用のないプログラムに限定 static 変数にアクセス不能 Managedオブジェクト(通常の参照型)にアクセス不能 強い制約を守らないとBurstできない
7.ザ・Burst Burstまとめ • ランタイムに差し替え可能 • 副作用なし • bssセクションなし • 常に[BurstCompile]するよう心がけよう • 自然にプログラムが安全になる • Debug.Logは使えない(Jobでは可能) • [BusrtDiscard]を関数に設置することでBusrt時に消滅させられる(デバッグ用) • e.g. [BurstDiscard] public static void print<T>(T v) { Debug.Log(v); }
“ 8.マルチスレッドのおさらい ”
8.マルチスレッドのおさらい スレッドのリソース スレッドA スタック レジスタ スレッドB スタック レジスタ スレッドC スタック レジスタ スレッドD スタック レジスタ スタックとレジスタはスレッド固有 プログラム言語におけるオート変数に相当
8.マルチスレッドのおさらい CPUひとつでマルチスレッド スレッドA スタック レジスタ スレッドB スタック レジスタ リソースを切り替える (コンテキストスイッチ) スレッドAのレジスタを退避 スレッドBのレジスタを復帰 スレッド切り替えはカーネルのお仕事 いつ行われるのか?
8.マルチスレッドのおさらい
C言語のコンパイル結果
.section
__TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl __Z3addii
## -- Begin function
_Z3addii
.p2align
4, 0x90
__Z3addii:
## @_Z3addii
.cfi_startproc
## BB#0:
pushq %rbp
int add(int a, int b)
Lcfi0:
{
Lcfi1:
return a + b;
}
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
movl
%edi, -4(%rbp)
movl
%esi, -8(%rbp)
movl
-4(%rbp), %esi
addl
-8(%rbp), %esi
movl
%esi, %eax
popq %rbp
コンパイル方式のプログラムは
例外なくこの形式で実行される
オレンジ色は
有効な命令
retq
.cfi_endproc
## -- End function
.subsections_via_symbols
8.マルチスレッドのおさらい スレッド切り替えの発生ポイント .section __TEXT,__text,regular,pure_instructions .macosx_version_min 10, 13 .globl __Z3addii ## -- Begin function _Z3addii .p2align 4, 0x90 __Z3addii: ## @_Z3addii .cfi_startproc ## BB#0: pushq %rbp Lcfi0: .cfi_def_cfa_offset 16 Lcfi1: .cfi_offset %rbp, -16 いつでも発生しうる movq %rsp, %rbp Lcfi2: .cfi_def_cfa_register %rbp movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl -4(%rbp), %esi addl -8(%rbp), %esi movl %esi, %eax popq %rbp retq .cfi_endproc 大丈夫なように作る スレッドセーフ ## -- End function .subsections_via_symbols
8.マルチスレッドのおさらい スレッドセーフでない とは? スレッドD スレッドA スタック レジスタ スタック レジスタ メモリ スレッドC スレッドB スタック レジスタ スタック レジスタ オート変数以外のメモリにアクセスしているのに マルチスレッドを考慮していない状態
8.マルチスレッドのおさらい この関数はスレッドセーフか? int add(int a, int b) { return a + b; }
8.マルチスレッドのおさらい この関数はスレッドセーフか? int add(int a, int b) { return a + b; } セーフ! オート変数しか使用していない 副作用がない
8.マルチスレッドのおさらい スレッドセーフでない関数 int add(int a) { static int p = 0; p += a; return p; } アウト! static変数はスレッド間で共有されてしまう 副作用がある
8.マルチスレッドのおさらい 何が起きるのか? static int p; p += a; 1:pをメモリから読み込む 2:読み込んだ値にaを加える 3:加えた値をpに書き込む
8.マルチスレッドのおさらい 何が起きるのか? static int p; p += a; スレッドA 1:pをメモリから読み込む スレッドB スレッド切り替え 1:pをメモリから読み込む 2:読み込んだ値にaを加える 2:読み込んだ値にaを加える 3:加えた値をpに書き込む スレッド切り替え 3:加えた値をpに書き込む スレッドBの処理が無効となるバグ
8.マルチスレッドのおさらい スレッドセーフまとめ • オート変数を使う • スタックおよびレジスタはスレッドローカル • 副作用が必要ならスレッドセーフ、機構を実装する(後述) • マルチコアも事情は同じ • L1キャッシュの動作に関してはマルチコア特有の事情を考慮する • 学習としてはまずマルチスレッドを押さえよう
“ 9.最重要! ComponentDataArray ”
9.最重要! ComponentDataArray Systemの記述 public class MySystem : JobComponentSystem { // ここで Inject を受ける記述 protected override void JobHandle OnUpdate(JobHandle inputDep) { // ここで実行 } } ECSの実行機構
9.最重要! ComponentDataArray Systemの記述 public class MySystem : JobComponentSystem { // ここで Inject を受ける記述 protected override void JobHandle OnUpdate(JobHandle inputDep) { // ここで実行 } } MonoBehaviour→JobComponentSystem Update→OnUpdate に対応 ただしインスタンスは見えない
9.最重要! ComponentDataArray Inject する public class MySystem : JobComponentSystem { struct MyGroup { ComponentDataArray<Foo> foos; … } [Inject] MyGroup group; protected override void JobHandle OnUpdate(JobHandle inputDep) { // ここで実行 } } InjectによりComponentDataArrayの準備が整う OnUpdateの前に毎フレーム働く
9.最重要! ComponentDataArray Jobに渡ったComponentDataArray public class MySystem : JobComponentSystem { … struct MyJob : IJobParallelFor { ComponentDataArray<Foo> foos; public void Exec(int i) { // 処理 } } protected override void JobHandle OnUpdate(JobHandle inputDep) { // ここでComponentDataArrayを渡したジョブをスケジュール } }
9.最重要! ComponentDataArray ComponentDataArray は只者ではない ComponentDataArray<Foo> foos; Chunk … Chunk Foo f = foos[i] Chunk 不連続なデータに効率的にアクセス 内部のキャッシュ機構で効率化
9.最重要! ComponentDataArray ComponentDataArray は只者ではない ComponentDataArray<Foo> foos; Chunk … Chunk Foo f = foos[i] Chunk 不連続なデータに効率的にアクセス 内部のキャッシュ機構で効率化 ちょっと待て・・・それはスレッドセーフなのか?
9.最重要! ComponentDataArray スレッドC スレッドB スレッドA 実行スレッドごとにコピーが作られる(!) ComponentDataArray<Foo> foos; Chunk … Foo f = foos[i] ComponentDataArray<Foo> foos; Chunk … Foo f = foos[i] ComponentDataArray<Foo> foos; Chunk … Foo f = foos[i] よって内部キャッシュはスレッドローカル スレッドセーフ!
9.最重要!
ComponentDataArray
ReadとWriteの記述
struct MyJob : IJobParallelFor {
ComponentDataArray<Foo> foos;
public void Exec(int i) {
Foo foo = foos[i];
// read
// 処理
foos[i] = foo;
// write
}
}
インデクサは単純なアクセサではない
foos[i].value = 10;
のような書き方は許可されない
9.最重要! ComponentDataArray ReadとWriteの記述 struct MyJob : IJobParallelFor { [ReadOnly] ComponentDataArray<Foo> foos; public void Exec(int i) { Foo foo = foos[i]; // read // 処理 foos[i] = foo; // write } } [ReadOnly] の場合は コピーを取り出すのみ
9.最重要!
ComponentDataArray
ReadとWriteの記述
struct MyJob : IJobParallelFor {
[WriteOnly] ComponentDataArray<Foo> foos;
public void Exec(int i) {
Foo foo = foos[i];
// read
// 処理
foos[i] = foo;
// write
}
}
[WriteOnly] の場合は
新規生成を書き込むのみ
e.g. foos[i] = new Foo();
9.最重要! ComponentDataArray ComponentDataArrayまとめ • Nativeコンテナの一種 • [Inject]されることで利用可能 • 内部のデータは必ずしも連続ではない • とびとびというわけでもなく、時々ギャップがあるぐらい • 内部キャッシュで効率化 • それでもスレッドセーフ • ジョブ構造体ごと実行スレッド用にコピーされており、キャッシュが独立しているため • アクセスは構造体単位。部分的なメンバーへのアクセスはできない • ただし JobComponentSystem を使うと特定箇所のアクセスが可能 • JobComponentSystem が速度的に有利(とドキュメントに書いてある)な理由 • ただしJobComponentSystemは3種類のIComponentDataしか扱えない(現在)
“ 10.IComponentDataを見極める ”
10.IComponentDataを見極める IComponentDataに書けるもの/書けないもの public unsafe struct MyComponent : IComponentData { public int i; // OK public string str; // NG (参照型) public byte* ptr; // OK (要unsafe) public bool flg; // NG (non blittable) public NativeArray na; // NG public fixed int fa[256]; // OK (要unsafe) } 要するにblittableかどうか
10.IComponentDataを見極める IComponentDataまとめ • blittable なもののみ定義可能 • unsafe にすれば pointerも可能 • unsafe はもう恐れない(基盤側で使いまくっている) • NativeArray は定義不可 • 内部のDisposeSentinelという開発支援機構がReference型 • 自作のNativeContainerはその点に気をつければ定義可能 • 固定サイズバッファ(Fixed Size Buffers)はunsafeで定義可能 • ただし型の制限が厳しい。以下の型のみで float3 などは使用できない(C#の制限) • bool, byte, short, int, long, char, sbyte, ushort, uint, ulong, float or double • fixed bool flg[32]; • C#上は許可されるがnon-blittableなのでIComponentData上はランタイムエラー
11.デモ
“ 12.JobだってEntityを生成したい ”
12.JobだってEntityを生成したい 並列実行中にEntityを生成・削除できるものなのか? できません AddComponentやRemoveComponentも不可
12.JobだってEntityを生成したい 並列実行でEntityを追加するには コマンドキューに積む: EntityCommandBuffer すべてのJobを停止してキューを実行する: BarrierSystem 正確にはキュー実行時にEntityManagerがJob完了待ちをする
12.JobだってEntityを生成したい BarrierSystem の Inject public class MySystem : ComponentSystem { struct MyGroup { ComponentDataArray<Foo> foos; } [Inject] MyGroup group; class MyBarrier : BarrierSystem {} [Inject] MyBarrier barrier; System内に定義して[Inject]する
12.JobだってEntityを生成したい BarrierSystemからEntityCommandBufferを生成 [Inject] MyGroup group; class MyBarrier : BarrierSystem {} [Inject] MyBarrier barrier; protected override JobHandle OnUpdate(JobHandle dep) { var combuf = barrier.CreateCommandBuffer(); var job = new MyJob { commandbuffer = (EntityCommandBuffer.Concurrent)combuf; Concurrent版をJobに渡す
12.JobだってEntityを生成したい EntityCommandBufferは遅延実行用のキュー internal enum ECBCommand { CreateEntity, DestroyEntity, AddComponent, RemoveComponent, SetComponent, AddSharedComponentData, SetSharedComponentData } 可能なのはこれだけ
12.JobだってEntityを生成したい 遅延実行ということは・・・ public void CreateEntity(EntityArchetype archetype) 返り値は得られないよね public void SetComponent<T>(T component) where T : struct, IComponentData 直前に生成したEntityに対して実行
12.JobだってEntityを生成したい EntityCommandBuffer まとめ • 遅延実行であることに留意しよう • EntityManagerと同じインタフェースにはならない • Concurrentによる並列実行が可能(できるようになったのはわりと最近) • Entityの構造変化(Create, Destroy, AddComponent, RemoveComponent, …)は ジョブの完全停止が必要 • BarrierSystemで実行 EntityManager tips • Entityが格納されるバッファはCapacityを越えると倍にしてmemcpyする • 起動時に EntityManager.EntityCapacity を想定最大値に設定するとよい
12.JobだってEntityを生成したい BarrierSystem まとめ • Injectすることで定義した場所(System)の直後に実行される • Barrierは継承して使用すべし • 継承したBarrierにはわかりやすい名前をつけておくとよい • プロファイラでBarrierの名前が見える
“ 13.Entityを追いかけろ! ComponentDataFromEntity ”
13.Entityを追いかけろ! ComponentDataFromEntity ロックオンターゲットを追尾したい public struct LaserData : IComponentData { … public Entity target_entity_; … 追尾レーザーはロックオンターゲットのEntityを保持
13.Entityを追いかけろ! ComponentDataFromEntity ComponentDataFromEntity public class LaserSystem : JobComponentSystem { [Inject] [ReadOnly] public ComponentDataFromEntity<Position> position_list_from_entity_; … Inject で利用可能に … var target_pos = position_list_from_entity_[ld.target_entity_]; Entityを引数にしてComponent取得
13.Entityを追いかけろ!
ComponentDataFromEntity
ExistsでEntityの存在確認がジョブからでも可能
struct MyJob : IJobParallelFor {
…
[ReadOnly] public ComponentDataFromEntity<Position> position_list_from_entity_;
…
public void Execute(int i) {
if (!position_list_from_entity_.Exists(ld.target_entity_)) {
…
ゲームプログラムにおける超重要機能
13.Entityを追いかけろ! ComponentDataFromEntity ComponentDataFromEntity まとめ • Nativeコンテナの一種 • Injectして使う • 任意のEntityからComponentを取得できる • ジョブの中でEntityの存在をチェック可能
“ 14.実装! トレイルレンダリング ”
14.実装! トレイルレンダリング いかにしてIComponentDataで点群を実現するか Fixed Size Buffersは・・・ public fixed float points[256]; IComponentDataはコピーされる宿命にある あまり大きなバッファを起きたくない そもそもfloat3などを書けない
14.実装! トレイルレンダリング そんなときはFixedArrayArray(注) public struct TrailPoint { public float3 position; ただの構造体を定義 public float3 normal; } archetype_ = entity_manager.CreateArchetype( typeof(Destroyable) … , ComponentType.FixedArray(typeof(TrailPoint), 64)); Archetype定義でFixedArrayを使う
14.実装! トレイルレンダリング 〜注意〜 FixedArrayArray は廃止されます より高機能なDynamicBuffersが登場 とにかくEntityにもバッファを配置できますよ、という話
14.実装! トレイルレンダリング FixedArrayArray を Injectして使う public class TrailSystem : JobComponentSystem{ struct Group { public FixedArrayArray<TrailPoint> trail_points_list_; } [Inject] Group group_; 固定長配列がEntityぶんある 二次元配列
14.実装! トレイルレンダリング Jobで使用する例 struct MyJob : IJobParallelFor { public FixedArrayArray<TrailPoint> trail_points_list_; public void Execute(int i) { var trail_points = trail_points_list_[i]; for (var j = 0; j < trail_points.Length; ++j) { var tp = trail_points[ j]; … Entity毎に固定長配列を入手できた このとき trail_points はNativeArray
14.実装! トレイルレンダリング FixedArrayArrayまとめ • 複数のコンポーネントを同一Entityに配置したい場合にも使用を検討してよい • インデクサで返されるのはNativeArray • 保持メモリの部分参照をNativeArray化 • 確保されたものではないのでDisposeを呼ぶとクラッシュする(はず) • 部分アクセスが可能 • IComponentData単位でのコピーから逃れられる • Componentと同じくチャンクに格納される • Entityひとつのサイズ上限(現在は16KiBらしい)があるため無茶はできない • 要素数が多ければ固定サイズバッファより利便性も効率も高いはず • FixedArrayArrayは廃止されるが当面使う分には問題ない • あとでDynamicBuffersに置き換えれば良い
“ 15.Jobテクをもうひとつだけ ”
15.Jobテクをもうひとつだけ Jobに参照型(Managedメモリ)は渡せない struct MyJob : IJob { public List<Vector3> vertices; // 実行時エラー … しかし static 関数にはアクセスできる
15.Jobテクをもうひとつだけ
どこかにオブジェクトを定義しておけば
public static class Foo {
static List<Vector3> list;
public static GetList() { return list; }
…
struct MyJob : IJob {
public void Execute() {
List<Vector3> list = Foo.GetList();
…
staticでアクセス可能(!)
15.Jobテクをもうひとつだけ トレイルレンダラの頂点生成 MainThread vertices normals uvs List<T>に頂点情報を生成 triangles
15.Jobテクをもうひとつだけ Job化 MainThread vertices WorkerThread vertices WorkerThread normals WorkerThread uvs WorkerThread triangles normals uvs triangles メインスレッドからオフロード&並列化
15.Jobテクをもうひとつだけ 頂点生成をバックスレッド化する前
15.Jobテクをもうひとつだけ 頂点生成をバックスレッド化した後 0.49ms 0.024ms
15.Jobテクをもうひとつだけ C# Job System テクニックまとめ • Busrtをあきらめれば意外と制限は緩い • static や Managedメモリを扱う場合は慎重に • なるべくBurstを心がけて安全に • 並列Entity生成において、Material(つまりManagedオブジェクト)を保持する ISharedComponentDataを扱いたい場合はBurstを切ることで可能になる
“ 16.コリジョン作ってみた ”
16.コリジョン作ってみた コリジョン仕様 プレイヤー エネミー プレイヤー弾 エネミー弾 特定の組み合わせで総当たり
16.コリジョン作ってみた 戦略 空間を分割して総当たりコストを減らす NativeMultiHashmap を使う
16.コリジョン作ってみた 所属グリッドを量子化で決定、ハッシュ値を取る ハッシュ値の算出はサンプルの実装を利用 https://github.com/Unity-Technologies/EntityComponentSystemSamples.git EntityComponentSystemSamples/Samples/Assets/GameCode/Samples.Common/Utilities/HashUtility.cs
16.コリジョン作ってみた 同一ハッシュ値に従い、コリジョン判定を実行 ハッシュの衝突については問題にならない
16.コリジョン作ってみた NativeMultiHashMap キー 101 231 473 534 343 56 472 354 Positionからハッシュ値を生成してキーにする 同一ハッシュに属する集団を取得できる
16.コリジョン作ってみた コリジョンには大きさがある 周囲のグリッドにまたがっている場合も考慮
16.コリジョン作ってみた
コリジョンを取る前にNativeArrayにコピーしてしまうテクニック
var colliders = new NativeArray<SphereCollider>(player_group.Length,
Allocator.TempJob,
NativeArrayOptions.UninitializedMemory);
var job = new CopyComponentData<SphereCollider> {
Source = player_group.sphere_collider_list,
Results = colliders,
};
並列アクセス用のクラスを別にする
var handle = job.Schedule(len, 32, handle);
ComponentDataArrayの使用が早期に終了するメリット
16.コリジョン作ってみた コリジョン実装まとめ • 総当たりコストを軽減する • NativeMultiHashMapは並列処理に対応 • Positionからハッシュ値を生成する • ハッシュ値がぶつかっても問題ない。運悪くコリジョン判定が増えるだけ • コリジョンに大きさがあることに注意 • 周辺セルにも登録する。多重登録は問題ない • 場合によっては総当たりで十分な場合もある • 一時的なNativeArrayにコピーすることで効率を上げられる可能性 • 数が多い場合など • NativeArrayはIL2CPP環境において最大効率(zero-overhead)を実現している
“ 17.自作せよ! Nativeコンテナ ”
17.自作せよ! Nativeコンテナ Spriteをまとめて描画したい これ 並列で格納したい 描画発行を高速化したい
17.自作せよ! Nativeコンテナ Nativeコンテナ典型パターンその1 public unsafe struct NativeBucket { byte* m_HeaderBlock; } ポインタでデータを扱う 基本的にコピーして扱われるので 変動するデータはすべてポインタの先に格納する
17.自作せよ! Nativeコンテナ Nativeコンテナ典型パターンその2 public unsafe struct NativeBucket { byte* m_HeaderBlock; public unsafe struct Concurrent { byte* m_HeaderBlock; } } 並列アクセス用のクラスを別にする 並列アクセス用のインタフェースを Concurrent だけに置く
17.自作せよ! Nativeコンテナ Nativeコンテナ典型パターンその3 public unsafe struct NativeBucket { byte* m_HeaderBlock; public unsafe struct Concurrent { byte* m_HeaderBlock; } [NativeSetThreadIndex] int m_ThreadIndex; } 秘技[NativeSetThreadIndex] で実行中のスレッドを特定する
17.自作せよ! Nativeコンテナ スレッドがわかっていれば・・ スレッドA スレッドB メモリ スレッドC スレッドD アクセス領域を分離できる スレッドセーフ JobsUtility.MaxJobThreadCountでスレッド最大数を取得可能
17.自作せよ! Nativeコンテナ さらに効率を求めて スレッドA スレッドB スレッドC 64bytes 64bytes メモリ 64bytes 64bytes スレッドD アクセス領域を64bytes以上離す L1キャッシュの競合を避ける JobsUtility.CacheLineSizeでキャッシュラインサイズを取得可能
17.自作せよ! Nativeコンテナ NativeBucket実装 スレッドA スレッドB スレッドC スレッドD data data data memcpy data data data 描画 最大効率で並列追加&描画コマンド生成
17.自作せよ! Nativeコンテナ Nativeコンテナ自作まとめ • ポインタの理解は必須 • C言語のポインタを習得すればOK • ジョブの使用に限定することでNativeSetThreadIndexによるスレッドセーフが容易に実現 • 汎用的に作る場合lock-freeアルゴリズム(後述)の理解も必要 • 事実上lockは使えない • メモリをスレッドで分離する実装だとCapacityなどで事前確保するのは難しい • NativeQueueにCapacityがない理由 • DisposeSentinelなどのセーフティ機構は可能な限り取り入れる • 積極的にTestを書こう • レースコンディションを確認するためのtorture testは必須 • Test RunnerのEditModeでもジョブが動作する(!) • 始める際にはサンプルを参照: • https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/Samples/Assets/NativeCounterDemo/NativeCounter.cs •実装が見えているNativeQueue.csなども参考になる •NativeArrayは公開ソースコードで UnityCsReference/Runtime/Export/NativeArray/NativeArray.cs
“ 18.選ぶのは8個 ”
18.選ぶのは8個 ロックオンターゲット ロックオンできる数には上限を設けたい
18.選ぶのは8個 条件を満たす物体のうち8個の状態を変更したい それを並列で実行したい 難易度高
18.選ぶのは8個 ちょ・・ ・・・ あらよ 8匹ま で 難易度高! いけるッ いらすとや
18.選ぶのは8個 NativeLimitedCounter を自作 public unsafe struct NativeLimitedCounter { [NativeDisableUnsafePtrRestriction] int* m_Counter; 元気よくポインタを定義 [NativeDisable… はジョブに渡すために必要
18.選ぶのは8個
NativeLimitedCounter のコンストラクタ
public unsafe struct NativeLimitedCounter {
[NativeDisableUnsafePtrRestriction] int* m_Counter;
Allocator m_AllocatorLabel;
public NativeLimitedCounter(Allocator label) {
m_AllocatorLabel = label;
long size = UnsafeUtility.SizeOf<int>();
m_Counter = (int*)UnsafeUtility.Malloc(size, 16, label);
UnsafeUtility.MemClear(m_Counter, size);
}
UnsafeUtility.Malloc で確保
18.選ぶのは8個 Interlocked.CompareExchange public bool TryIncrement(int inclusive_limit) { int current = *m_Counter; while (current < inclusive_limit) { int next = current + 1; int prev = Interlocked.CompareExchange(ref *m_Counter, next, current); if (prev == current) { return true; } else { current = prev; } } return false; } いわゆる CAS(Compare And Swap)
18.選ぶのは8個
18.選ぶのは8個 Lock Free アルゴリズム public bool TryIncrement(int inclusive_limit) { int current = *m_Counter; // メモリから読み込み(4byteなのでatomicと想定) while (current < inclusive_limit) { // 引数の制限以下なら int next = current + 1; // 増加後の値 int prev = Interlocked.CompareExchange(ref *m_Counter, next, current); if (prev == current) { return true; } else { // 成功、終了 // 想定と異なる。再入が発生している! current = prev; } } return false; } // 想定通りだった // 比較するレジスタをメモリの状態で更新してやり直し
18.選ぶのは8個 ジョブ実装 [BurstCompile] struct LockonJob : IJobParallelFor { public NativeLimitedCounter lc; … public void Execute(int i) { bool can_lock = … if (can_lock) { if (lc.TryIncrement(8)) { ロックオン処理 … TryIncrementが成功したらロックオン成立
18.選ぶのは8個 Systemで生成してジョブに流す public class PlayerSystem : JobComponentSystem { NativeLimitedCounter lockon_limit_; protected override JobHandle OnUpdate(JobHandle inputDeps) { if (lockon_limit_.IsCreated) { lockon_limit_.Dispose(); } lockon_limit_ = new NativeLimitedCounter(Allocator.TempJob); 現在の数 を計上 上限まで ロックオン処理 var sumup_job = new SumupJob { lockon_limit_ = lockon_limit_, }; handle_ = sumup_job.Schedule(lockon_group_.Length, 32, handle_); var lockon_job = new LockonJob { lockon_limit_ = lockon_limit_,}; handle_ = lockon_job.Schedule(lockon_group_.Length, 32, handle_); 毎フレーム生成して使用する
18.選ぶのは8個 Lock Freeアルゴリズムまとめ • Nativeコンテナの思想に沿って実装しよう • ポインタ先のメモリを意識しよう • Interlocked.CompareExchange の性質を理解しよう • テストを書こう
“ 19.伝えたい Entity間通信 ”
19.伝えたい Entity間通信 一体に複数のターゲットが配置される ロックオンの基本
19.伝えたい Entity間通信 親子関係は仕組みが用意されている(注) archetype_ = entity_manager.CreateArchetype( 子 typeof(Destroyable) , typeof(LockTarget) 親 , typeof(TransformParent) , typeof(LocalPosition) 子 , typeof(LocalRotation) , typeof(Position) 子 , typeof(Rotation) , typeof(TransformMatrix)); TransformSystemが解決してくれる
14.実装! トレイルレンダリング 〜注意〜 親子関係の仕組みは刷新されます より効率の良い LocalToWorld などの仕組みに とにかく親子関係は作れますよ、という話
19.伝えたい Entity間通信 ダメージ情報をどうやって親に伝えるか 子 親 レーザーが命中! 子 子 レーザーも並列で計算されている
19.伝えたい
Entity間通信
AtomicFloatの実装
public float add(float value) {
float current = UnsafeUtility.ReadArrayElement<float>(m_Data, 0 /* index */);
int currenti = math.asint(current);
for (;;) {
float next = current + value;
int nexti = math.asint(next);
int prev = Interlocked.CompareExchange(ref *(int*)m_Data, nexti, currenti);
if (prev == currenti) {
return next;
} else {
currenti = prev;
current = math.asfloat(prev);
}
}
}
Interlocked.Add は整数のみなので自作
19.伝えたい Entity間通信 LockFreeオブジェクトを作る 子 親 子 子 多数必要なので工夫する
19.伝えたい Entity間通信 自作 AtomicFloatResource AtomicFloatResource AtomicFloat AtomicFloat AtomicFloat AtomicFloatを振り出せる仕組み
19.伝えたい Entity間通信 共有メモリをLockFreeでアクセス 子 レーザーが命中! AtomicFloat 親 AtomicFloat 子 AtomicFloat 子 AtomicFloat ダメージの累積を正しく認識
19.伝えたい Entity間通信 AtomicFloatの工夫 破棄にAtomicFloatResourceが必要 破棄済みかどうかがわからない AtomicFloatResourceを埋め込む
19.伝えたい Entity間通信 AtomicFloatResourceを埋め込めば自己破壊可能 AtomicFloatResource AtomicFloatResource AtomicFloat AtomicFloat AtomicFloatResource AtomicFloatResource AtomicFloatResource AtomicFloat AtomicFloatResource AtomicFloatResource AtomicFloatResource AtomicFloatResource Nativeコンテナの思想で作ってあればコピー可能
19.伝えたい Entity間通信 Entity間通信オブジェクトAtomicFloatResourceまとめ • 小さなメモリ確保をしないことで断片化を抑制 • 破棄済みかを自己判定できる仕組み • スロット再利用との区別もしている • 破棄も任意のタイミングで呼べる • DisposeはDestructor的なタイミングで呼ばれるべき • 現状は明示的にDisposeを呼んでいるため解放もれなどのミスを犯しやすい • ISystemStateComponentData を利用した ReactiveSystem を検討すべき • .NetではCompareExchangeにfloat(Single)版が用意されているが、これの使用をBurstは許 可していない。cmpxchg(x86系)などの命令がfloatレジスタに対応していないので、いずれにせ よ整数レジスタに値を移動する必要があるからだろう。そもそもInterlocked.Addにfloat版が用 意されていないのも同様の理屈と思われる。なおUnity.Mathematicsには整数レジスタと浮動 小数レジスタの相互変換をサポートする math.asfloat および math.asint が用意されている
“ 20.ECS & Job が奏でる未来 ”
20.ECS & Job が奏でる未来 •従来の仕組みがなくなるわけではない •作りやすさは正義! •性能はゲームの商品価値のほんの一部 •パフォーマンスに目をとらわれ過ぎぬよう •「Jobを使用しない単体のECS」は決して難しくはない •ECSだけでも検討の価値あり •高みを目指せば難易度は上がる •Unityを使いながら高みを目指せる •パフォーマンスを理由にエンジンを採用する時代に
20.ECS & Job が奏でる未来 性能例:今回のデモを iPhone X で実行 4msec
おしまい