Se ha denunciado esta presentación.
Utilizamos tu perfil de LinkedIn y tus datos de actividad para personalizar los anuncios y mostrarte publicidad más relevante. Puedes cambiar tus preferencias de publicidad en cualquier momento.
CPUを使い切れ!
Entity Component System(通称ECS)
が切り開く新しいプログラミング
ユニティ・テクノロジーズ・ジャパン合同会社
フィールドエンジニア 安原 祐二
おすすめ資料
https://www.youtube.com/watch?v=eA2t8HBtRzg
https://www.slideshare.net/UnityTechnologiesJapan/gtmf2018tokyounity3
ハ...
本講演の対象バージョン
Unity2018.2.2f1
{
"dependencies": {
"com.unity.entities": "0.0.12-preview.8",
"com.unity.package-manager-ui": ...
本講演のプロジェクト
https://github.com/Unity-Technologies/AnotherThreadECS
https://youtu.be/nOeOrJRf5NY 
デモ動画
1. 復習 class と struct
2. Nativeコンテナってなんだ
3. C# Job System 概要
4. CPUキャッシュのおさらい
5. ECSの思想に迫る
6. Entity生成 step by step
7. ザ・Bu...
“ ”
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
ポインタは等間隔
サイズの異なる実体を別の場所に置く
参照型の配列
メモリはバラバラ
 1.復習 class と struct
0
1
2
3
4
5
6
7
サイズ一定
メモリは
カタマリになる
値型の配列
“ ”
2.Nativeコンテナってなんだ
 
• NativeArray
• 通常配列
• ポインタからの変換可能(ConvertExistingDataToNativeArray)
• NativeSlice
• NativeArrayの部分切り出しが可能
• ポインタからの変換可能...
 2.Nativeコンテナってなんだ
var a = new NativeArray<MyStruct>(32, Allocator.Persistent);
生成
解放
Allocator.Persistent
Allocator.Temp
...
 2.Nativeコンテナってなんだ
var a = new NativeArray<MyStruct>(32, Allocator.Persistent);
var b = new MyStruct[32];
a.CopyTo(b);
生成
...
 2.Nativeコンテナってなんだ
Nativeコンテナまとめ
• Unityが用意した便利コンテナ
• Managedメモリを使用しない
• Gabage Collection とは無縁
• 実装はMalloc/Freeを使用してメモリ確保...
“ ”
3.C# Job System 概要
 3.C# Job System 概要
var th = new System.Threading.Thread(Func);
th.Start();
・これまでもスレッドは自作できた
・Unityが用意したWorkerThreadを使えるよう...
 3.C# Job System 概要
Worker Thread で動いている
 3.C# Job System 概要
・Main Thread 動作中に実行可能
・メニーコアの有効利用
Camera.Renderなどの不可避な処理の裏はたいてい空いている
WorkerThreadのメリット
 3.C# Job System のおさらい
Camera.Render
Camera.Renderの裏で動いている
 3.C# Job System のおさらい
struct AJob : IJobParallelFor {
public NativeArray<Vector3> positions;
public void Execute(int i) {...
 3.C# Job System のおさらい
struct AJob : IJobParallelFor {
public NativeArray<Vector3> positions;
public void Execute(int id) ...
 3.C# Job System のおさらい
struct AJob : IJobParallelFor {
public NativeArray<Vector3> positions;
public void Execute(int id) ...
 3.C# Job System のおさらい
InvalidOperationException: The native container has been
declared as [WriteOnly] in the job, but yo...
 3.C# Job System のおさらい
Scheduleの引数にJobHandleを渡す
void Update() {
var ajob = new AJob() { positions = m_Positions, };
var bj...
 3.C# Job System のおさらい
C# Job Systemまとめ
• 危険なマルチスレッドを回避
• 属性[ReadOnly][WriteOnly]でランタイムチェック
• 依存や同期が簡単に書ける
• 命令の発行(Schedul...
“ ”
4.CPUキャッシュのおさらい
 4.CPUキャッシュのおさらい
L2キャッシュ
メモリ
コア
L1
コア
L1
コア
L1
コア
L1
コア
L1
コア
L1
コア
L1
コア
L1
遅い
速い
メモリ・キャッシュ・メニーコア
 4.CPUキャッシュのおさらい
L2キャッシュ
メモリ
コア
L1
コア
L1
コア
L1
コア
L1
コア
L1
コア
L1
コア
L1
コア
L1
遅い
速い
x10
x20
x200
(※数値は例)
メモリ・キャッシュ・メニーコア
 4.CPUキャッシュのおさらい
L2キャッシュ
メインメモリ
コア
L164
64
コ
1byteの使用でも
近傍の64bytesが
キャッシュに乗る
配置を最適化すれば
100倍ぐらい速いはず(?)
キャッシュラインは64bytes
“ ”
5.ECSの思想に迫る
 5.ECSの思想に迫る
Enemy
Position
Rotation
Health
Rigidbody
Position
Rigidbody
Rotation
Position
Rotation
Health
Rigidbody
Posit...
 5.ECSの思想に迫る
Enemy
Position
Rotation
Health
Rigidbody
Position
Rigidbody
Rotation
Position
Rotation
Health
Rigidbody
Posit...
 5.ECSの思想に迫る
Enemy
Position
Rotation
Health
Rigidbody
Position
Rigidbody
Rotation
Position
Rotation
Health
Rigidbody
Posit...
 5.ECSの思想に迫る
Enemy
Position
Rotation
Health
Rigidbody
Position
Rigidbody
Rotation
Position
Rotation
Health
Rigidbody
Posit...
 5.ECSの思想に迫る
Enemy
Position
Rotation
Health
Rigidbody
Position
Rigidbody
Rotation
Position
Rotation
Health
Rigidbody
Posit...
 5.ECSの思想に迫る
ECSまとめ
• メモリ配置を考慮
• ECSに従えば効率の良いデータ配置を実現できる
• System で同一要素を一括処理
“ ”
6.Entity生成 step by step
 6.Entity生成 step by step
public struct RigidbodyPosition : IComponentData {
public float3 velocity;
public float3 accelera...
 6.Entity生成 step by step
namespace Unity.Entities
{
public interface IComponentData
{
}
何も書いてない
…/com.unity.entities@0.0.1...
 6.Entity生成 step by step
namespace Unity.Transforms
{
public struct Position : IComponentData
{
public float3 Value;
}
}
…...
 6.Entity生成 step by step
通常: struct Vector3
struct Position
struct Velocity
struct AimDirection
様々な意味に使う
構造体が特定の意味ECS:
Vec...
 6.Entity生成 step by step
var entity_manager = World.Active.GetOrCreateManager<EntityManager>();
arche_type = entity_manage...
 6.Entity生成 step by step
var entity = entity_manager.CreateEntity(arche_type);
var entity = entity_manager.CreateEntity(ar...
 6.Entity生成 step by step
Entity生成まとめ
• コンパイル時にComponentDataを定義
• 実行時にArchetypeを定義
• ArchetypeからEntityを生成
• Entityは単なるIDでしか...
“ ”
7.ザ・Burst
 7.ザ・Burst
高速コード(主にSIMD化による)を生成する特殊コンパイラ
[BurstCompile]
struct AJob : IJobParallelFor {
public NativeArray<Vector3> positi...
 7.ザ・Burst
いつコンパイルしているのか?
なんと実行時
エディタ実行の場合
タイミングをコンソールに出すと・・
 7.ザ・Burst
差し替えられるということは
副作用のないプログラムに限定
強い制約を守らないとBurstできない
static 変数にアクセス不能
Managedオブジェクト(通常の参照型)にアクセス不能
 7.ザ・Burst
Burstまとめ
• ランタイムに差し替え可能
• 副作用なし
• bssセクションなし
• 常に[BurstCompile]するよう心がけよう
• 自然にプログラムが安全になる
• Debug.Logは使えない(Jobで...
“ ”
8.マルチスレッドのおさらい
 8.マルチスレッドのおさらい
スレッドA
スレッドのリソース
スタック レジスタ
スタックとレジスタはスレッド固有
プログラム言語におけるオート変数に相当
スレッドB スタック レジスタ
スレッドC スタック レジスタ
スレッドD スタック ...
 8.マルチスレッドのおさらい
スレッドA
CPUひとつでマルチスレッド
スタック レジスタ
リソースを切り替える(コンテキストスイッチ)
スレッド切り替えはカーネルのお仕事
スレッドB スタック レジスタ
スレッドAのレジスタを退避
スレッド...
 8.マルチスレッドのおさらい
int add(int a, int b)
{
return a + b;
}
.section __TEXT,__text,regular,pure_instructions
.macosx_version_m...
 8.マルチスレッドのおさらい
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl __Z3addii ## -- Begin f...
 8.マルチスレッドのおさらい
スレッドセーフでない とは?
オート変数以外のメモリにアクセスしているのに
マルチスレッドを考慮していない状態
スレッドA スタック レジスタ
メモリ
スレッドB スタック レジスタ
スレッドD スタック レジス...
 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.マルチスレッドのおさらい
何が起きるのか?
1:pをメモリから読み込む
2:読み込んだ値にaを加える
3:加えた値をpに書き込む
1:pをメモリから読み込む
2:読み込んだ値にaを加える
3:加えた値をpに書き込む
スレッドA スレッドB...
 8.マルチスレッドのおさらい
スレッドセーフまとめ
• オート変数を使う
• スタックおよびレジスタはスレッドローカル
• 副作用が必要ならスレッドセーフ、機構を実装する(後述)
• マルチコアも事情は同じ
• L1キャッシュの動作に関しては...
“ ”
9.最重要! ComponentDataArray
 9.最重要! ComponentDataArray
Systemの記述
public class MySystem : JobComponentSystem {
// ここで Inject を受ける記述
protected override ...
 9.最重要! ComponentDataArray
Systemの記述
MonoBehaviour→JobComponentSystem
Update→OnUpdate に対応
ただしインスタンスは見えない
public class MySy...
 9.最重要! ComponentDataArray
Inject する
InjectによりComponentDataArrayの準備が整う
OnUpdateの前に毎フレーム働く
public class MySystem : JobCompo...
 9.最重要! ComponentDataArray
Jobに渡ったComponentDataArray
public class MySystem : JobComponentSystem {
…
struct MyJob : IJobPar...
 9.最重要! ComponentDataArray
ComponentDataArray は只者ではない
不連続なデータに効率的にアクセス
Chunk
Chunk
Chunk
ComponentDataArray<Foo> foos;
…
F...
 9.最重要! ComponentDataArray
ComponentDataArray は只者ではない
不連続なデータに効率的にアクセス
Chunk
Chunk
Chunk
ComponentDataArray<Foo> foos;
…
F...
 9.最重要! ComponentDataArray
実行スレッドごとにコピーが作られる(!)
よって内部キャッシュはスレッドローカル
Chunk
Chunk
Chunk
ComponentDataArray<Foo> foos;
…
Foo ...
 9.最重要! ComponentDataArray
ReadとWriteの記述
struct MyJob : IJobParallelFor {
ComponentDataArray<Foo> foos;
public void Exec(i...
 9.最重要! ComponentDataArray
ReadとWriteの記述
struct MyJob : IJobParallelFor {
[ReadOnly] ComponentDataArray<Foo> foos;
public ...
 9.最重要! ComponentDataArray
ReadとWriteの記述
struct MyJob : IJobParallelFor {
[WriteOnly] ComponentDataArray<Foo> foos;
public...
 9.最重要! ComponentDataArray
ComponentDataArrayまとめ
• Nativeコンテナの一種
• [Inject]されることで利用可能
• 内部のデータは必ずしも連続ではない
• とびとびというわけでもなく、...
“ ”
10.IComponentDataを見極める
 10.IComponentDataを見極める
IComponentDataに書けるもの/書けないもの
public unsafe struct MyComponent : IComponentData {
public int i; // O...
 10.IComponentDataを見極める
IComponentDataまとめ
• blittable なもののみ定義可能
• unsafe にすれば pointerも可能
• unsafe はもう恐れない(基盤側で使いまくっている)
• ...
11.デモ
“ ”
12.JobだってEntityを生成したい
 12.JobだってEntityを生成したい
並列実行中にEntityを生成・削除できるものなのか?
できません
AddComponentやRemoveComponentも不可
 12.JobだってEntityを生成したい
並列実行でEntityを追加するには
コマンドキューに積む:
すべてのJobを停止してキューを実行する:
EntityCommandBuffer
BarrierSystem
正確にはキュー実行時にEn...
 12.JobだってEntityを生成したい
BarrierSystem の Inject
public class MySystem : ComponentSystem {
struct MyGroup {
ComponentDataArra...
 12.JobだってEntityを生成したい
BarrierSystemからEntityCommandBufferを生成
[Inject] MyGroup group;
class MyBarrier : BarrierSystem {}
[In...
 12.JobだってEntityを生成したい
EntityCommandBufferは遅延実行用のキュー
internal enum ECBCommand {
CreateEntity,
DestroyEntity,
AddComponent,
...
 12.JobだってEntityを生成したい
遅延実行ということは・・・
public void CreateEntity(EntityArchetype archetype)
返り値は得られないよね
public void SetCompon...
 12.JobだってEntityを生成したい
EntityCommandBuffer まとめ
• 遅延実行であることに留意しよう
• EntityManagerと同じインタフェースにはならない
• Concurrentによる並列実行が可能(できる...
 12.JobだってEntityを生成したい
BarrierSystem まとめ
• Injectすることで定義した場所(System)の直後に実行される
• Barrierは継承して使用すべし
• 継承したBarrierにはわかりやすい名前を...
“ ”
13.Entityを追いかけろ!
ComponentDataFromEntity
 13.Entityを追いかけろ! ComponentDataFromEntity
ロックオンターゲットを追尾したい
追尾レーザーはロックオンターゲットのEntityを保持
public struct LaserData : IComponen...
 13.Entityを追いかけろ! ComponentDataFromEntity
ComponentDataFromEntity
public class LaserSystem : JobComponentSystem
{
[Inject]...
 13.Entityを追いかけろ! ComponentDataFromEntity
ExistsでEntityの存在確認がジョブからでも可能
ゲームプログラムにおける超重要機能
struct MyJob : IJobParallelFor {
...
 13.Entityを追いかけろ! ComponentDataFromEntity
ComponentDataFromEntity まとめ
• Nativeコンテナの一種
• Injectして使う
• 任意のEntityからComponentを...
“ ”
14.実装! トレイルレンダリング
 14.実装! トレイルレンダリング
いかにしてIComponentDataで点群を実現するか
Fixed Size Buffersは・・・
public fixed float points[256];
IComponentDataはコピーされる...
 14.実装! トレイルレンダリング
そんなときはFixedArrayArray(注)
ただの構造体を定義
public struct TrailPoint {
public float3 position;
public float3 nor...
 14.実装! トレイルレンダリング
〜注意〜
より高機能なDynamicBuffersが登場
FixedArrayArray は廃止されます
とにかくEntityにもバッファを配置できますよ、という話
 14.実装! トレイルレンダリング
FixedArrayArray を Injectして使う
public class TrailSystem : JobComponentSystem{
struct Group {
public Fixed...
 14.実装! トレイルレンダリング
Jobで使用する例
struct MyJob : IJobParallelFor {
public FixedArrayArray<TrailPoint> trail_points_list_;
publi...
 14.実装! トレイルレンダリング
FixedArrayArrayまとめ
• 複数のコンポーネントを同一Entityに配置したい場合にも使用を検討してよい
• インデクサで返されるのはNativeArray
• 保持メモリの部分参照をNati...
“ ”
15.Jobテクをもうひとつだけ
 15.Jobテクをもうひとつだけ
Jobに参照型(Managedメモリ)は渡せない
struct MyJob : IJob {
public List<Vector3> vertices; // 実行時エラー
…
しかし static 関数に...
 15.Jobテクをもうひとつだけ
どこかにオブジェクトを定義しておけば
struct MyJob : IJob {
public void Execute() {
List<Vector3> list = Foo.GetList();
…
s...
 
MainThread
15.Jobテクをもうひとつだけ
トレイルレンダラの頂点生成
List<T>に頂点情報を生成
vertices normals uvs triangles
 
WorkerThread
15.Jobテクをもうひとつだけ
Job化
メインスレッドからオフロード&並列化
vertices
MainThread
WorkerThread normals
WorkerThread uvs
WorkerTh...
 15.Jobテクをもうひとつだけ
頂点生成をバックスレッド化する前
 15.Jobテクをもうひとつだけ
頂点生成をバックスレッド化した後
0.49ms
0.024ms
 15.Jobテクをもうひとつだけ
C# Job System テクニックまとめ
• Busrtをあきらめれば意外と制限は緩い
• static や Managedメモリを扱う場合は慎重に
• なるべくBurstを心がけて安全に
• 並列Ent...
“ ”
16.コリジョン作ってみた
 16.コリジョン作ってみた
コリジョン仕様
特定の組み合わせで総当たり
プレイヤー エネミー
エネミー弾プレイヤー弾
 16.コリジョン作ってみた
戦略
空間を分割して総当たりコストを減らす
NativeMultiHashmap を使う
 16.コリジョン作ってみた
所属グリッドを量子化で決定、ハッシュ値を取る
ハッシュ値の算出はサンプルの実装を利用
https://github.com/Unity-Technologies/EntityComponentSystemSampl...
 16.コリジョン作ってみた
同一ハッシュ値に従い、コリジョン判定を実行
ハッシュの衝突については問題にならない
 16.コリジョン作ってみた
NativeMultiHashMap
Positionからハッシュ値を生成してキーにする
101 231 473 534 343 56 472 354キー
同一ハッシュに属する集団を取得できる
 16.コリジョン作ってみた
周囲のグリッドにまたがっている場合も考慮
コリジョンには大きさがある
 16.コリジョン作ってみた
コリジョンを取る前にNativeArrayにコピーしてしまうテクニック
並列アクセス用のクラスを別にする
var colliders = new NativeArray<SphereCollider>(player...
 16.コリジョン作ってみた
コリジョン実装まとめ
• 総当たりコストを軽減する
• NativeMultiHashMapは並列処理に対応
• Positionからハッシュ値を生成する
• ハッシュ値がぶつかっても問題ない。運悪くコリジョン判定...
“ ”
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 ...
 17.自作せよ! Nativeコンテナ
Nativeコンテナ典型パターンその3
秘技[NativeSetThreadIndex]
で実行中のスレッドを特定する
public unsafe struct NativeBucket {
byte*...
 17.自作せよ! Nativeコンテナ
スレッドがわかっていれば・・
スレッドA
スレッドB
スレッドC
スレッドD
メモリ
アクセス領域を分離できる スレッドセーフ
JobsUtility.MaxJobThreadCountでスレッド最大数...
 17.自作せよ! Nativeコンテナ
さらに効率を求めて
スレッドA
スレッドB
スレッドC
スレッドD
メモリ
64bytes
64bytes
64bytes
64bytes
アクセス領域を64bytes以上離す
L1キャッシュの競合を避...
 17.自作せよ! Nativeコンテナ
NativeBucket実装
スレッドA
スレッドB
スレッドC
スレッドD
最大効率で並列追加&描画コマンド生成
data
data
data
data data data
memcpy
描画
 17.自作せよ! Nativeコンテナ
Nativeコンテナ自作まとめ
• ポインタの理解は必須
• C言語のポインタを習得すればOK
• ジョブの使用に限定することでNativeSetThreadIndexによるスレッドセーフが容易に実現
...
“ ”
18.選ぶのは8個
 18.選ぶのは8個
ロックオンターゲット
ロックオンできる数には上限を設けたい
 18.選ぶのは8個
条件を満たす物体のうち8個の状態を変更したい
それを並列で実行したい
難易度高
 
8匹まで
18.選ぶのは8個
難易度高!
あらよ
ちょ・・
いけるッ
・・・
いらすとや
 18.選ぶのは8個
NativeLimitedCounter を自作
元気よくポインタを定義
public unsafe struct NativeLimitedCounter {
[NativeDisableUnsafePtrRestric...
 18.選ぶのは8個
NativeLimitedCounter のコンストラクタ
UnsafeUtility.Malloc で確保
public unsafe struct NativeLimitedCounter {
[NativeDisab...
 18.選ぶのは8個
Interlocked.CompareExchange
いわゆる CAS(Compare And Swap)
public bool TryIncrement(int inclusive_limit) {
int curr...
 18.選ぶのは8個
 18.選ぶのは8個
Lock Free アルゴリズム
public bool TryIncrement(int inclusive_limit) {
int current = *m_Counter; // メモリから読み込み(4byteなの...
 18.選ぶのは8個
ジョブ実装
TryIncrementが成功したらロックオン成立
[BurstCompile]
struct LockonJob : IJobParallelFor {
public NativeLimitedCounter...
 18.選ぶのは8個
Systemで生成してジョブに流す
毎フレーム生成して使用する
public class PlayerSystem : JobComponentSystem {
NativeLimitedCounter lockon_li...
 18.選ぶのは8個
Lock Freeアルゴリズムまとめ
• Nativeコンテナの思想に沿って実装しよう
• ポインタ先のメモリを意識しよう
• Interlocked.CompareExchange の性質を理解しよう
• テストを書こう
“ ”
19.伝えたい Entity間通信
 19.伝えたい Entity間通信
一体に複数のターゲットが配置される
ロックオンの基本
 19.伝えたい Entity間通信
親子関係は仕組みが用意されている(注)
TransformSystemが解決してくれる
親
子
子
子
archetype_ = entity_manager.CreateArchetype(
typeof...
 14.実装! トレイルレンダリング
〜注意〜
より効率の良い LocalToWorld などの仕組みに
親子関係の仕組みは刷新されます
とにかく親子関係は作れますよ、という話
 19.伝えたい Entity間通信
ダメージ情報をどうやって親に伝えるか
レーザーも並列で計算されている
親
子
子
子
レーザーが命中!
 19.伝えたい Entity間通信
AtomicFloatの実装
public float add(float value) {
float current = UnsafeUtility.ReadArrayElement<float>(m_...
 19.伝えたい Entity間通信
LockFreeオブジェクトを作る
多数必要なので工夫する
親
子
子
子
 19.伝えたい Entity間通信
自作 AtomicFloatResource
AtomicFloatを振り出せる仕組み
AtomicFloatResource
AtomicFloat
AtomicFloat
AtomicFloat
 19.伝えたい Entity間通信
共有メモリをLockFreeでアクセス
ダメージの累積を正しく認識
親
子
子
子
レーザーが命中!
AtomicFloat
AtomicFloat
AtomicFloat
AtomicFloat
 19.伝えたい Entity間通信
AtomicFloatの工夫
AtomicFloatResourceを埋め込む
破棄にAtomicFloatResourceが必要
破棄済みかどうかがわからない
 19.伝えたい Entity間通信
AtomicFloatResourceを埋め込めば自己破壊可能
Nativeコンテナの思想で作ってあればコピー可能
AtomicFloatResource
AtomicFloat
AtomicFloat
A...
 19.伝えたい Entity間通信
Entity間通信オブジェクトAtomicFloatResourceまとめ
• 小さなメモリ確保をしないことで断片化を抑制
• 破棄済みかを自己判定できる仕組み
• スロット再利用との区別もしている
• 破...
“ ”
20.ECS & Job が奏でる未来
 20.ECS & Job が奏でる未来
•従来の仕組みがなくなるわけではない
•作りやすさは正義!
•性能はゲームの商品価値のほんの一部
•パフォーマンスに目をとらわれ過ぎぬよう
•「Jobを使用しない単体のECS」は決して難しくはない
•E...
 20.ECS & Job が奏でる未来
4msec
性能例:今回のデモを iPhone X で実行
おしまい
Próxima SlideShare
Cargando en…5
×

【CEDEC2018】CPUを使い切れ! Entity Component System(通称ECS) が切り開く新しいプログラミング

18.981 visualizaciones

Publicado el

2018/8/22に開催されたCEDEC2018の講演資料です。
講師:安原 祐二(ユニティ・テクノロジーズ・ジャパン合同会社)

Publicado en: Tecnología
  • Sé el primero en comentar

【CEDEC2018】CPUを使い切れ! Entity Component System(通称ECS) が切り開く新しいプログラミング

  1. 1. CPUを使い切れ! Entity Component System(通称ECS) が切り開く新しいプログラミング ユニティ・テクノロジーズ・ジャパン合同会社 フィールドエンジニア 安原 祐二
  2. 2. おすすめ資料 https://www.youtube.com/watch?v=eA2t8HBtRzg https://www.slideshare.net/UnityTechnologiesJapan/gtmf2018tokyounity3 ハードウェアの性能を活かす為の、Unityの新しい3つの機能 動画: スライド資料:
  3. 3. 本講演の対象バージョン Unity2018.2.2f1 { "dependencies": { "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", "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" ] } Packages/manifest.json→
  4. 4. 本講演のプロジェクト https://github.com/Unity-Technologies/AnotherThreadECS https://youtu.be/nOeOrJRf5NY  デモ動画
  5. 5. 1. 復習 class と struct 2. Nativeコンテナってなんだ 3. C# Job System 概要 4. CPUキャッシュのおさらい 5. ECSの思想に迫る 6. Entity生成 step by step 7. ザ・Burst 8. マルチスレッドのおさらい 9. 最重要! ComponentDataArray 10.IComponentDataを見極める 11.デモ 12.JobだってEntityを生成したい 13.Entityを追いかけろ! 14.実装! トレイルレンダリング 15.Jobテクをもうひとつだけ 16.コリジョン作ってみた 17.自作せよ! Nativeコンテナ 18.選ぶのは8個 19.伝えたい Entity間通信 20.ECS & Job が奏でる未来 本日のコース
  6. 6. “ ” 1.復習 class と struct
  7. 7.  1.復習 class と struct サイズ不定 (継承、文字列型…) class=参照型 サイズ固定 struct=値型 (int、Vector3…)
  8. 8.  1.復習 class と struct 0 1 2 3 4 5 6 7 そもそも配列は 等間隔が必須 参照型はサイズが異なるため 配列に格納できない 配列を考える
  9. 9.  1.復習 class と struct 0 1 2 3 4 5 6 7 ポインタは等間隔 サイズの異なる実体を別の場所に置く 参照型の配列 メモリはバラバラ
  10. 10.  1.復習 class と struct 0 1 2 3 4 5 6 7 サイズ一定 メモリは カタマリになる 値型の配列
  11. 11. “ ” 2.Nativeコンテナってなんだ
  12. 12.   • NativeArray • 通常配列 • ポインタからの変換可能(ConvertExistingDataToNativeArray) • NativeSlice • NativeArrayの部分切り出しが可能 • ポインタからの変換可能(ConvertExistingDataToNativeSlice) • NativeList • マルチスレッド書き込み非対応 • NativeArray化にメモリコピーを使用しない • Job中でのサイズ変更後の状態でNativeArray化できる ToDeferredJobArray • NativeQueue • いわゆるFIFO。マルチスレッド書き込み対応 • NativeHashmap, NativeMultiHashmap • Key-Value コンテナ。マルチスレッド書き込み対応 2.Nativeコンテナってなんだ Unmanaged(GCが起きない)メモリを使用したコンテナ
  13. 13.  2.Nativeコンテナってなんだ var a = new NativeArray<MyStruct>(32, Allocator.Persistent); 生成 解放 Allocator.Persistent Allocator.Temp Allocator.TempJob 永続的に使用可能 同じフレームで解放しないとエラー 4フレーム以内に解放しないとエラー a.Dispose(); 例:NativeArray フレームの概念が組み込まれている素晴らしさ
  14. 14.  2.Nativeコンテナってなんだ var a = new NativeArray<MyStruct>(32, Allocator.Persistent); var b = new MyStruct[32]; a.CopyTo(b); 生成 コピー (memcpyのloop) 今後NativeArray対応APIは増えていきます a.CopyFrom(b); とはいえ通常の配列を要求するUnityのAPIは多い・・・
  15. 15.  2.Nativeコンテナってなんだ Nativeコンテナまとめ • Unityが用意した便利コンテナ • Managedメモリを使用しない • Gabage Collection とは無縁 • 実装はMalloc/Freeを使用してメモリ確保 • structで定義されているが概念は参照型(スマートポインタ)に近い • IL2CPPを想定した効率化が計られている • 性能測定はビルド後のものを • ユーザの自作も設計に組み込まれている(後述)
  16. 16. “ ” 3.C# Job System 概要
  17. 17.  3.C# Job System 概要 var th = new System.Threading.Thread(Func); th.Start(); ・これまでもスレッドは自作できた ・Unityが用意したWorkerThreadを使えるように プロファイラで可視化 安全に書ける(エラーがたくさん出てくれる) 使いやすくなったThread
  18. 18.  3.C# Job System 概要 Worker Thread で動いている
  19. 19.  3.C# Job System 概要 ・Main Thread 動作中に実行可能 ・メニーコアの有効利用 Camera.Renderなどの不可避な処理の裏はたいてい空いている WorkerThreadのメリット
  20. 20.  3.C# Job System のおさらい Camera.Render Camera.Renderの裏で動いている
  21. 21.  3.C# Job System のおさらい struct AJob : IJobParallelFor { public NativeArray<Vector3> positions; public void Execute(int i) { var pos = positions[i]; pos.y += 1f; positions[i] = pos; } } IJobParallelForを実装してExecuteを定義 indexは供給される Jobが実行する Uniform的なもの Jobの書きかた
  22. 22.  3.C# Job System のおさらい struct AJob : IJobParallelFor { public NativeArray<Vector3> positions; public void Execute(int id) { var pos = positions[id]; pos.y += 1f; positions[id] = pos; } } 毎フレームScheduleを呼ぶ 読んでしまったら ajob は破棄して良い void Update() { var ajob = new AJob() { positions = m_Positions, }; var handle = ajob.Schedule(positions.Length, 8, ); } 生成 実行指令 データ参照 Jobの呼びかた
  23. 23.  3.C# Job System のおさらい struct AJob : IJobParallelFor { public NativeArray<Vector3> positions; public void Execute(int id) { var pos = positions[id]; pos.y += 1f; positions[id] = pos; } } idは供給される Jobが実行する Uniform的なもの Jobの書きかた 参照なので危険がつきまとう [ReadOnly]や[WriteOnly]属性をつける
  24. 24.  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> 本当にありがとうございます 出してくれるエラーの例
  25. 25.  3.C# Job System のおさらい Scheduleの引数にJobHandleを渡す 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(); } ジョブ発行時に依存関係を定義
  26. 26.  3.C# Job System のおさらい C# Job Systemまとめ • 危険なマルチスレッドを回避 • 属性[ReadOnly][WriteOnly]でランタイムチェック • 依存や同期が簡単に書ける • 命令の発行(Schedule)や同期(Complete)がメインスレッドからしか呼べない • デッドロックを起こせない
  27. 27. “ ” 4.CPUキャッシュのおさらい
  28. 28.  4.CPUキャッシュのおさらい L2キャッシュ メモリ コア L1 コア L1 コア L1 コア L1 コア L1 コア L1 コア L1 コア L1 遅い 速い メモリ・キャッシュ・メニーコア
  29. 29.  4.CPUキャッシュのおさらい L2キャッシュ メモリ コア L1 コア L1 コア L1 コア L1 コア L1 コア L1 コア L1 コア L1 遅い 速い x10 x20 x200 (※数値は例) メモリ・キャッシュ・メニーコア
  30. 30.  4.CPUキャッシュのおさらい L2キャッシュ メインメモリ コア L164 64 コ 1byteの使用でも 近傍の64bytesが キャッシュに乗る 配置を最適化すれば 100倍ぐらい速いはず(?) キャッシュラインは64bytes
  31. 31. “ ” 5.ECSの思想に迫る
  32. 32.  5.ECSの思想に迫る Enemy Position Rotation Health Rigidbody Position Rigidbody Rotation Position Rotation Health Rigidbody Position Rigidbody Rotation Renderer Transform Matrix Transform Matrix Player Renderer Position Rotation Health Transform Matrix Missile Renderer Position Rotation Health Rigidbody Position Rigidbody Rotation Transform Matrix Enemy Renderer Position Rotation Health Transform Matrix Missile Renderer Position Rotation Transform Matrix Bullet Renderer Position Rotation Transform Matrix Bullet Renderer 横の要素をメモリ的に近くに置く
  33. 33.  5.ECSの思想に迫る Enemy Position Rotation Health Rigidbody Position Rigidbody Rotation Position Rotation Health Rigidbody Position Rigidbody Rotation Renderer Transform Matrix Transform Matrix Player Renderer Position Rotation Health Transform Matrix Missile Renderer Position Rotation Health Rigidbody Position Rigidbody Rotation Transform Matrix Enemy Renderer Position Rotation Health Transform Matrix Missile Renderer Position Rotation Transform Matrix Bullet Renderer Position Rotation Transform Matrix Bullet Renderer Entity(物体)
  34. 34.  5.ECSの思想に迫る Enemy Position Rotation Health Rigidbody Position Rigidbody Rotation Position Rotation Health Rigidbody Position Rigidbody Rotation Renderer Transform Matrix Transform Matrix Player Renderer Position Rotation Health Transform Matrix Missile Renderer Position Rotation Health Rigidbody Position Rigidbody Rotation Transform Matrix Enemy Renderer Position Rotation Health Transform Matrix Missile Renderer Position Rotation Transform Matrix Bullet Renderer Position Rotation Transform Matrix Bullet Renderer ComponentData(要素)
  35. 35.  5.ECSの思想に迫る Enemy Position Rotation Health Rigidbody Position Rigidbody Rotation Position Rotation Health Rigidbody Position Rigidbody Rotation Renderer Transform Matrix Transform Matrix Player Renderer Position Rotation Health Transform Matrix Missile Renderer Position Rotation Health Rigidbody Position Rigidbody Rotation Transform Matrix Enemy Renderer Position Rotation Health Transform Matrix Missile Renderer Position Rotation Transform Matrix Bullet Renderer Position Rotation Transform Matrix Bullet Renderer System(共通部分に対する処理)
  36. 36.  5.ECSの思想に迫る Enemy Position Rotation Health Rigidbody Position Rigidbody Rotation Position Rotation Health Rigidbody Position Rigidbody Rotation Renderer Transform Matrix Transform Matrix Player Renderer Position Rotation Health Transform Matrix Missile Renderer Position Rotation Health Rigidbody Position Rigidbody Rotation Transform Matrix Enemy Renderer Position Rotation Health Transform Matrix Missile Renderer Position Rotation Transform Matrix Bullet Renderer Position Rotation Transform Matrix Bullet Renderer 例:RigidbodyPositionSystemが対象とするデータ
  37. 37.  5.ECSの思想に迫る ECSまとめ • メモリ配置を考慮 • ECSに従えば効率の良いデータ配置を実現できる • System で同一要素を一括処理
  38. 38. “ ” 6.Entity生成 step by step
  39. 39.  6.Entity生成 step by step public struct RigidbodyPosition : IComponentData { public float3 velocity; public float3 acceleration; public float damper; } IComponentData は interface 手順1/3・Componentの定義
  40. 40.  6.Entity生成 step by step namespace Unity.Entities { public interface IComponentData { } 何も書いてない …/com.unity.entities@0.0.12-preview.8/Unity.Entities/IComponentData.cs
  41. 41.  6.Entity生成 step by step namespace Unity.Transforms { public struct Position : IComponentData { public float3 Value; } } …/com.unity.entities@0.0.12-preview.8/Unity.Transforms/PositionComponent.cs ところで、なにか違和感を感じませんか? 例:Positionの定義
  42. 42.  6.Entity生成 step by step 通常: struct Vector3 struct Position struct Velocity struct AimDirection 様々な意味に使う 構造体が特定の意味ECS: Vector3 position; Vector3 velocity; Vector3 aimDirection; 構造体が型ではなく、具体的な意味になる
  43. 43.  6.Entity生成 step by step 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)); ふつうは起動時にやっておく 手順2/3・ArchType(型)を作る
  44. 44.  6.Entity生成 step by step var entity = entity_manager.CreateEntity(arche_type); 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で初期化 SetComponentDataがEntityManager経由! オブジェクト指向との思想の違い 手順3/3・CreateEntityでEntityを作成
  45. 45.  6.Entity生成 step by step Entity生成まとめ • コンパイル時にComponentDataを定義 • 実行時にArchetypeを定義 • ArchetypeからEntityを生成 • Entityは単なるIDでしかない • すべてを把握するEntityManager経由で操作する
  46. 46. “ ” 7.ザ・Burst
  47. 47.  7.ザ・Burst 高速コード(主にSIMD化による)を生成する特殊コンパイラ [BurstCompile] struct AJob : IJobParallelFor { public NativeArray<Vector3> positions; public void Execute(int id) { var pos = positions[id]; pos.y += 1f; positions[id] = pos; } } 何もかも速くなるわけではない ここがBurstCompileされる
  48. 48.  7.ザ・Burst いつコンパイルしているのか? なんと実行時 エディタ実行の場合 タイミングをコンソールに出すと・・
  49. 49.  7.ザ・Burst 差し替えられるということは 副作用のないプログラムに限定 強い制約を守らないとBurstできない static 変数にアクセス不能 Managedオブジェクト(通常の参照型)にアクセス不能
  50. 50.  7.ザ・Burst Burstまとめ • ランタイムに差し替え可能 • 副作用なし • bssセクションなし • 常に[BurstCompile]するよう心がけよう • 自然にプログラムが安全になる • Debug.Logは使えない(Jobでは可能) • [BusrtDiscard]を関数に設置することでBusrt時に消滅させられる(デバッグ用) • e.g. [BurstDiscard] public static void print<T>(T v) { Debug.Log(v); }
  51. 51. “ ” 8.マルチスレッドのおさらい
  52. 52.  8.マルチスレッドのおさらい スレッドA スレッドのリソース スタック レジスタ スタックとレジスタはスレッド固有 プログラム言語におけるオート変数に相当 スレッドB スタック レジスタ スレッドC スタック レジスタ スレッドD スタック レジスタ
  53. 53.  8.マルチスレッドのおさらい スレッドA CPUひとつでマルチスレッド スタック レジスタ リソースを切り替える(コンテキストスイッチ) スレッド切り替えはカーネルのお仕事 スレッドB スタック レジスタ スレッドAのレジスタを退避 スレッドBのレジスタを復帰 いつ行われるのか?
  54. 54.  8.マルチスレッドのおさらい int add(int a, int b) { return a + b; } .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 C言語のコンパイル結果 オレンジ色は 有効な命令 コンパイル方式のプログラムは 例外なくこの形式で実行される
  55. 55.  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 スレッド切り替えの発生ポイント いつでも発生しうる 大丈夫なように作る スレッドセーフ
  56. 56.  8.マルチスレッドのおさらい スレッドセーフでない とは? オート変数以外のメモリにアクセスしているのに マルチスレッドを考慮していない状態 スレッドA スタック レジスタ メモリ スレッドB スタック レジスタ スレッドD スタック レジスタ スレッドC スタック レジスタ
  57. 57.  8.マルチスレッドのおさらい この関数はスレッドセーフか? int add(int a, int b) { return a + b; }
  58. 58.  8.マルチスレッドのおさらい この関数はスレッドセーフか? int add(int a, int b) { return a + b; } セーフ! オート変数しか使用していない 副作用がない
  59. 59.  8.マルチスレッドのおさらい スレッドセーフでない関数 int add(int a) { static int p = 0; p += a; return p; } アウト! static変数はスレッド間で共有されてしまう 副作用がある
  60. 60.  8.マルチスレッドのおさらい 何が起きるのか? static int p; p += a; 1:pをメモリから読み込む 2:読み込んだ値にaを加える 3:加えた値をpに書き込む
  61. 61.  8.マルチスレッドのおさらい 何が起きるのか? 1:pをメモリから読み込む 2:読み込んだ値にaを加える 3:加えた値をpに書き込む 1:pをメモリから読み込む 2:読み込んだ値にaを加える 3:加えた値をpに書き込む スレッドA スレッドB スレッド切り替え スレッド切り替え スレッドBの処理が無効となるバグ static int p; p += a;
  62. 62.  8.マルチスレッドのおさらい スレッドセーフまとめ • オート変数を使う • スタックおよびレジスタはスレッドローカル • 副作用が必要ならスレッドセーフ、機構を実装する(後述) • マルチコアも事情は同じ • L1キャッシュの動作に関してはマルチコア特有の事情を考慮する • 学習としてはまずマルチスレッドを押さえよう
  63. 63. “ ” 9.最重要! ComponentDataArray
  64. 64.  9.最重要! ComponentDataArray Systemの記述 public class MySystem : JobComponentSystem { // ここで Inject を受ける記述 protected override void JobHandle OnUpdate(JobHandle inputDep) { // ここで実行 } } ECSの実行機構
  65. 65.  9.最重要! ComponentDataArray Systemの記述 MonoBehaviour→JobComponentSystem Update→OnUpdate に対応 ただしインスタンスは見えない public class MySystem : JobComponentSystem { // ここで Inject を受ける記述 protected override void JobHandle OnUpdate(JobHandle inputDep) { // ここで実行 } }
  66. 66.  9.最重要! ComponentDataArray Inject する InjectによりComponentDataArrayの準備が整う OnUpdateの前に毎フレーム働く public class MySystem : JobComponentSystem { struct MyGroup { ComponentDataArray<Foo> foos; … } [Inject] MyGroup group; protected override void JobHandle OnUpdate(JobHandle inputDep) { // ここで実行 } }
  67. 67.  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を渡したジョブをスケジュール } }
  68. 68.  9.最重要! ComponentDataArray ComponentDataArray は只者ではない 不連続なデータに効率的にアクセス Chunk Chunk Chunk ComponentDataArray<Foo> foos; … Foo f = foos[i] 内部のキャッシュ機構で効率化
  69. 69.  9.最重要! ComponentDataArray ComponentDataArray は只者ではない 不連続なデータに効率的にアクセス Chunk Chunk Chunk ComponentDataArray<Foo> foos; … Foo f = foos[i] 内部のキャッシュ機構で効率化 ちょっと待て・・・それはスレッドセーフなのか?
  70. 70.  9.最重要! ComponentDataArray 実行スレッドごとにコピーが作られる(!) よって内部キャッシュはスレッドローカル Chunk Chunk Chunk ComponentDataArray<Foo> foos; … Foo f = foos[i] ComponentDataArray<Foo> foos; … Foo f = foos[i] ComponentDataArray<Foo> foos; … Foo f = foos[i] スレッドセーフ! スレッドAスレッドBスレッドC
  71. 71.  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; のような書き方は許可されない インデクサは単純なアクセサではない
  72. 72.  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] の場合は コピーを取り出すのみ
  73. 73.  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();
  74. 74.  9.最重要! ComponentDataArray ComponentDataArrayまとめ • Nativeコンテナの一種 • [Inject]されることで利用可能 • 内部のデータは必ずしも連続ではない • とびとびというわけでもなく、時々ギャップがあるぐらい • 内部キャッシュで効率化 • それでもスレッドセーフ • ジョブ構造体ごと実行スレッド用にコピーされており、キャッシュが独立しているため • アクセスは構造体単位。部分的なメンバーへのアクセスはできない • ただし JobComponentSystem を使うと特定箇所のアクセスが可能 • JobComponentSystem が速度的に有利(とドキュメントに書いてある)な理由 • ただしJobComponentSystemは3種類のIComponentDataしか扱えない(現在)
  75. 75. “ ” 10.IComponentDataを見極める
  76. 76.  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かどうか
  77. 77.  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上はランタイムエラー
  78. 78. 11.デモ
  79. 79. “ ” 12.JobだってEntityを生成したい
  80. 80.  12.JobだってEntityを生成したい 並列実行中にEntityを生成・削除できるものなのか? できません AddComponentやRemoveComponentも不可
  81. 81.  12.JobだってEntityを生成したい 並列実行でEntityを追加するには コマンドキューに積む: すべてのJobを停止してキューを実行する: EntityCommandBuffer BarrierSystem 正確にはキュー実行時にEntityManagerがJob完了待ちをする
  82. 82.  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]する
  83. 83.  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に渡す
  84. 84.  12.JobだってEntityを生成したい EntityCommandBufferは遅延実行用のキュー internal enum ECBCommand { CreateEntity, DestroyEntity, AddComponent, RemoveComponent, SetComponent, AddSharedComponentData, SetSharedComponentData } 可能なのはこれだけ
  85. 85.  12.JobだってEntityを生成したい 遅延実行ということは・・・ public void CreateEntity(EntityArchetype archetype) 返り値は得られないよね public void SetComponent<T>(T component) where T : struct, IComponentData 直前に生成したEntityに対して実行
  86. 86.  12.JobだってEntityを生成したい EntityCommandBuffer まとめ • 遅延実行であることに留意しよう • EntityManagerと同じインタフェースにはならない • Concurrentによる並列実行が可能(できるようになったのはわりと最近) • Entityの構造変化(Create, Destroy, AddComponent, RemoveComponent, …)は ジョブの完全停止が必要 • BarrierSystemで実行 • Entityが格納されるバッファはCapacityを越えると倍にしてmemcpyする • 起動時に EntityManager.EntityCapacity を想定最大値に設定するとよい EntityManager tips
  87. 87.  12.JobだってEntityを生成したい BarrierSystem まとめ • Injectすることで定義した場所(System)の直後に実行される • Barrierは継承して使用すべし • 継承したBarrierにはわかりやすい名前をつけておくとよい • プロファイラでBarrierの名前が見える
  88. 88. “ ” 13.Entityを追いかけろ! ComponentDataFromEntity
  89. 89.  13.Entityを追いかけろ! ComponentDataFromEntity ロックオンターゲットを追尾したい 追尾レーザーはロックオンターゲットのEntityを保持 public struct LaserData : IComponentData { … public Entity target_entity_; …
  90. 90.  13.Entityを追いかけろ! ComponentDataFromEntity ComponentDataFromEntity public class LaserSystem : JobComponentSystem { [Inject] [ReadOnly] public ComponentDataFromEntity<Position> position_list_from_entity_; … … var target_pos = position_list_from_entity_[ld.target_entity_]; Entityを引数にしてComponent取得 Inject で利用可能に
  91. 91.  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_)) { …
  92. 92.  13.Entityを追いかけろ! ComponentDataFromEntity ComponentDataFromEntity まとめ • Nativeコンテナの一種 • Injectして使う • 任意のEntityからComponentを取得できる • ジョブの中でEntityの存在をチェック可能
  93. 93. “ ” 14.実装! トレイルレンダリング
  94. 94.  14.実装! トレイルレンダリング いかにしてIComponentDataで点群を実現するか Fixed Size Buffersは・・・ public fixed float points[256]; IComponentDataはコピーされる宿命にある あまり大きなバッファを起きたくない そもそもfloat3などを書けない
  95. 95.  14.実装! トレイルレンダリング そんなときはFixedArrayArray(注) ただの構造体を定義 public struct TrailPoint { public float3 position; public float3 normal; } archetype_ = entity_manager.CreateArchetype( typeof(Destroyable) … , ComponentType.FixedArray(typeof(TrailPoint), 64)); Archetype定義でFixedArrayを使う
  96. 96.  14.実装! トレイルレンダリング 〜注意〜 より高機能なDynamicBuffersが登場 FixedArrayArray は廃止されます とにかくEntityにもバッファを配置できますよ、という話
  97. 97.  14.実装! トレイルレンダリング FixedArrayArray を Injectして使う public class TrailSystem : JobComponentSystem{ struct Group { public FixedArrayArray<TrailPoint> trail_points_list_; } [Inject] Group group_; 固定長配列がEntityぶんある 二次元配列
  98. 98.  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
  99. 99.  14.実装! トレイルレンダリング FixedArrayArrayまとめ • 複数のコンポーネントを同一Entityに配置したい場合にも使用を検討してよい • インデクサで返されるのはNativeArray • 保持メモリの部分参照をNativeArray化 • 確保されたものではないのでDisposeを呼ぶとクラッシュする(はず) • 部分アクセスが可能 • IComponentData単位でのコピーから逃れられる • Componentと同じくチャンクに格納される • Entityひとつのサイズ上限(現在は16KiBらしい)があるため無茶はできない • 要素数が多ければ固定サイズバッファより利便性も効率も高いはず • FixedArrayArrayは廃止されるが当面使う分には問題ない • あとでDynamicBuffersに置き換えれば良い
  100. 100. “ ” 15.Jobテクをもうひとつだけ
  101. 101.  15.Jobテクをもうひとつだけ Jobに参照型(Managedメモリ)は渡せない struct MyJob : IJob { public List<Vector3> vertices; // 実行時エラー … しかし static 関数にはアクセスできる
  102. 102.  15.Jobテクをもうひとつだけ どこかにオブジェクトを定義しておけば struct MyJob : IJob { public void Execute() { List<Vector3> list = Foo.GetList(); … staticでアクセス可能(!) public static class Foo { static List<Vector3> list; public static GetList() { return list; } …
  103. 103.   MainThread 15.Jobテクをもうひとつだけ トレイルレンダラの頂点生成 List<T>に頂点情報を生成 vertices normals uvs triangles
  104. 104.   WorkerThread 15.Jobテクをもうひとつだけ Job化 メインスレッドからオフロード&並列化 vertices MainThread WorkerThread normals WorkerThread uvs WorkerThread triangles vertices normals uvs triangles
  105. 105.  15.Jobテクをもうひとつだけ 頂点生成をバックスレッド化する前
  106. 106.  15.Jobテクをもうひとつだけ 頂点生成をバックスレッド化した後 0.49ms 0.024ms
  107. 107.  15.Jobテクをもうひとつだけ C# Job System テクニックまとめ • Busrtをあきらめれば意外と制限は緩い • static や Managedメモリを扱う場合は慎重に • なるべくBurstを心がけて安全に • 並列Entity生成において、Material(つまりManagedオブジェクト)を保持する ISharedComponentDataを扱いたい場合はBurstを切ることで可能になる
  108. 108. “ ” 16.コリジョン作ってみた
  109. 109.  16.コリジョン作ってみた コリジョン仕様 特定の組み合わせで総当たり プレイヤー エネミー エネミー弾プレイヤー弾
  110. 110.  16.コリジョン作ってみた 戦略 空間を分割して総当たりコストを減らす NativeMultiHashmap を使う
  111. 111.  16.コリジョン作ってみた 所属グリッドを量子化で決定、ハッシュ値を取る ハッシュ値の算出はサンプルの実装を利用 https://github.com/Unity-Technologies/EntityComponentSystemSamples.git EntityComponentSystemSamples/Samples/Assets/GameCode/Samples.Common/Utilities/HashUtility.cs
  112. 112.  16.コリジョン作ってみた 同一ハッシュ値に従い、コリジョン判定を実行 ハッシュの衝突については問題にならない
  113. 113.  16.コリジョン作ってみた NativeMultiHashMap Positionからハッシュ値を生成してキーにする 101 231 473 534 343 56 472 354キー 同一ハッシュに属する集団を取得できる
  114. 114.  16.コリジョン作ってみた 周囲のグリッドにまたがっている場合も考慮 コリジョンには大きさがある
  115. 115.  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の使用が早期に終了するメリット
  116. 116.  16.コリジョン作ってみた コリジョン実装まとめ • 総当たりコストを軽減する • NativeMultiHashMapは並列処理に対応 • Positionからハッシュ値を生成する • ハッシュ値がぶつかっても問題ない。運悪くコリジョン判定が増えるだけ • コリジョンに大きさがあることに注意 • 周辺セルにも登録する。多重登録は問題ない • 場合によっては総当たりで十分な場合もある • 一時的なNativeArrayにコピーすることで効率を上げられる可能性 • 数が多い場合など • NativeArrayはIL2CPP環境において最大効率(zero-overhead)を実現している
  117. 117. “ ” 17.自作せよ! Nativeコンテナ
  118. 118.  17.自作せよ! Nativeコンテナ Spriteをまとめて描画したい 並列で格納したい 描画発行を高速化したい これ
  119. 119.  17.自作せよ! Nativeコンテナ Nativeコンテナ典型パターンその1 ポインタでデータを扱う public unsafe struct NativeBucket { byte* m_HeaderBlock; } 基本的にコピーして扱われるので 変動するデータはすべてポインタの先に格納する
  120. 120.  17.自作せよ! Nativeコンテナ Nativeコンテナ典型パターンその2 並列アクセス用のクラスを別にする public unsafe struct NativeBucket { byte* m_HeaderBlock; public unsafe struct Concurrent { byte* m_HeaderBlock; } } 並列アクセス用のインタフェースを Concurrent だけに置く
  121. 121.  17.自作せよ! Nativeコンテナ Nativeコンテナ典型パターンその3 秘技[NativeSetThreadIndex] で実行中のスレッドを特定する public unsafe struct NativeBucket { byte* m_HeaderBlock; public unsafe struct Concurrent { byte* m_HeaderBlock; } [NativeSetThreadIndex] int m_ThreadIndex; }
  122. 122.  17.自作せよ! Nativeコンテナ スレッドがわかっていれば・・ スレッドA スレッドB スレッドC スレッドD メモリ アクセス領域を分離できる スレッドセーフ JobsUtility.MaxJobThreadCountでスレッド最大数を取得可能
  123. 123.  17.自作せよ! Nativeコンテナ さらに効率を求めて スレッドA スレッドB スレッドC スレッドD メモリ 64bytes 64bytes 64bytes 64bytes アクセス領域を64bytes以上離す L1キャッシュの競合を避ける JobsUtility.CacheLineSizeでキャッシュラインサイズを取得可能
  124. 124.  17.自作せよ! Nativeコンテナ NativeBucket実装 スレッドA スレッドB スレッドC スレッドD 最大効率で並列追加&描画コマンド生成 data data data data data data memcpy 描画
  125. 125.  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
  126. 126. “ ” 18.選ぶのは8個
  127. 127.  18.選ぶのは8個 ロックオンターゲット ロックオンできる数には上限を設けたい
  128. 128.  18.選ぶのは8個 条件を満たす物体のうち8個の状態を変更したい それを並列で実行したい 難易度高
  129. 129.   8匹まで 18.選ぶのは8個 難易度高! あらよ ちょ・・ いけるッ ・・・ いらすとや
  130. 130.  18.選ぶのは8個 NativeLimitedCounter を自作 元気よくポインタを定義 public unsafe struct NativeLimitedCounter { [NativeDisableUnsafePtrRestriction] int* m_Counter; [NativeDisable… はジョブに渡すために必要
  131. 131.  18.選ぶのは8個 NativeLimitedCounter のコンストラクタ UnsafeUtility.Malloc で確保 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); }
  132. 132.  18.選ぶのは8個 Interlocked.CompareExchange いわゆる CAS(Compare And Swap) 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; }
  133. 133.  18.選ぶのは8個
  134. 134.  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; }
  135. 135.  18.選ぶのは8個 ジョブ実装 TryIncrementが成功したらロックオン成立 [BurstCompile] struct LockonJob : IJobParallelFor { public NativeLimitedCounter lc; … public void Execute(int i) { bool can_lock = … if (can_lock) { if (lc.TryIncrement(8)) { ロックオン処理 …
  136. 136.  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_); 現在の数 を計上 上限まで ロックオン処理
  137. 137.  18.選ぶのは8個 Lock Freeアルゴリズムまとめ • Nativeコンテナの思想に沿って実装しよう • ポインタ先のメモリを意識しよう • Interlocked.CompareExchange の性質を理解しよう • テストを書こう
  138. 138. “ ” 19.伝えたい Entity間通信
  139. 139.  19.伝えたい Entity間通信 一体に複数のターゲットが配置される ロックオンの基本
  140. 140.  19.伝えたい Entity間通信 親子関係は仕組みが用意されている(注) TransformSystemが解決してくれる 親 子 子 子 archetype_ = entity_manager.CreateArchetype( typeof(Destroyable) , typeof(LockTarget) , typeof(TransformParent) , typeof(LocalPosition) , typeof(LocalRotation) , typeof(Position) , typeof(Rotation) , typeof(TransformMatrix));
  141. 141.  14.実装! トレイルレンダリング 〜注意〜 より効率の良い LocalToWorld などの仕組みに 親子関係の仕組みは刷新されます とにかく親子関係は作れますよ、という話
  142. 142.  19.伝えたい Entity間通信 ダメージ情報をどうやって親に伝えるか レーザーも並列で計算されている 親 子 子 子 レーザーが命中!
  143. 143.  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 は整数のみなので自作
  144. 144.  19.伝えたい Entity間通信 LockFreeオブジェクトを作る 多数必要なので工夫する 親 子 子 子
  145. 145.  19.伝えたい Entity間通信 自作 AtomicFloatResource AtomicFloatを振り出せる仕組み AtomicFloatResource AtomicFloat AtomicFloat AtomicFloat
  146. 146.  19.伝えたい Entity間通信 共有メモリをLockFreeでアクセス ダメージの累積を正しく認識 親 子 子 子 レーザーが命中! AtomicFloat AtomicFloat AtomicFloat AtomicFloat
  147. 147.  19.伝えたい Entity間通信 AtomicFloatの工夫 AtomicFloatResourceを埋め込む 破棄にAtomicFloatResourceが必要 破棄済みかどうかがわからない
  148. 148.  19.伝えたい Entity間通信 AtomicFloatResourceを埋め込めば自己破壊可能 Nativeコンテナの思想で作ってあればコピー可能 AtomicFloatResource AtomicFloat AtomicFloat AtomicFloat AtomicFloatResource AtomicFloatResource AtomicFloatResource AtomicFloatResource AtomicFloatResource AtomicFloatResource AtomicFloatResource AtomicFloatResource
  149. 149.  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 が用意されている
  150. 150. “ ” 20.ECS & Job が奏でる未来
  151. 151.  20.ECS & Job が奏でる未来 •従来の仕組みがなくなるわけではない •作りやすさは正義! •性能はゲームの商品価値のほんの一部 •パフォーマンスに目をとらわれ過ぎぬよう •「Jobを使用しない単体のECS」は決して難しくはない •ECSだけでも検討の価値あり •高みを目指せば難易度は上がる •Unityを使いながら高みを目指せる •パフォーマンスを理由にエンジンを採用する時代に
  152. 152.  20.ECS & Job が奏でる未来 4msec 性能例:今回のデモを iPhone X で実行
  153. 153. おしまい

×