【CEDEC2017】C#JobSystem を使った...

55
CJobSystem を使った Unity流マルチスレッドプログラミング ユニティ・テクノロジーズ・ジャパン合同会社 エバンジェリスト 伊藤

Transcript of 【CEDEC2017】C#JobSystem を使った...

C#JobSystem を使った Unity流マルチスレッドプログラミング

ユニティ・テクノロジーズ・ジャパン合同会社 エバンジェリスト伊藤 周

• 今回紹介するC# Job Systemはまだ発展段階 • リリースでは多少の差異が出る可能性がある • C# Job Systemの概念を知ってほしい • プログラマ以外は理解不能

諸注意

• 従来のマルチスレッドプログラミング • C# Job System の概要

• Let’s read codes. (コードを読む) • Let’s make a mistake. (間違ってみる) • Let’s try “C# Job Compiler” (コンパイラを体験) • Let’s implement. (実装してみる)

• まとめ

アジェンダ

従来のマルチスレッドプログラミングの話

マルチスレッド プログラミングは

好きですか?

私は嫌いです

• レースコンディション対策が嫌だ • 面倒臭い • コードが汚い、可読性が低い • 間違っても気づきにくい

MTPのここが嫌だ その1

Aが使うよ

Aが使うよBが使うよ

A

AB

write

write read

• デッドロックが嫌だ • 面倒臭い • コードが汚い、可読性が低い • 間違うと無限ループ

MTPのここが嫌だ その2

A

AB

B

待ち待ち

• 難解なところが嫌だ • mutex:lockとか • アトミック変数とか

MTPのここが嫌だ その3

• デバッグが嫌だ • 正常に”動いてしまったり”する • 無限ループになったりする • 突然ハングアップしたりする • リリース後にバグが判明したりする

MTPのここが嫌だ その4

私には無理だ!

そんなあなたに C# Job System

C# Job System の概要

43伊藤周の年齢

116

116倍Boid シミュレーションを

マルチスレッドで 8 core CPUで動かした場合の速度倍

Demo

• 簡潔に書ける • GCフリー • 安全 • 高速な新コンパイラ

C# Job System の特徴

• Data Oriented Programming • データとビヘイビア(振舞い)の分離 • struct(構造体)コンポーネントの導入

• Job Component System の用意 • 簡潔に書けるようにマネージャーを用意

特徴1 簡潔に書ける

• GCをいかにさせないか • NativeArrayの導入     

• 以下の感じで確保

• 以下の感じで解放(自分で)

特徴2 GCフリー

var src = new NativeArray<float>(500, Allocator.Temp);

src.Dispose();

 要素数 アロケーターの種類   ↓     ↓

• 他のNativeArrayファミリー

特徴2 GCフリー

struct NativeArray<Value> // 配列 struct NativeList<Value> // リスト。追加削除が容易 struct NativeSlice<Value> // 一部を切り取れる struct NativeHashmap<Key, Value> // Dictionary「 struct NativeMultiHashmap<Key, Value> //複数Dictionary

• エラーで指摘してくれる • 落ちることはない • レースコンディション、デッドロックは起こり得ない • “Sandbox”

特徴3 安全

• C#→[Mono]→IL→[C# Job Compiler]→内部的な Domain Model →[最適化]→[LLVM]→実行形式

• 10倍〜20倍高速になる • 電池消費の軽減 • Why faster?

• SIMD命令の有効利用 • 正確さとパフォーマンスのトレードオフ

特徴4 高速な新コンパイラ

Let’s read C# Job System codes!

• IJob~でジョブを定義 • Execute にジョブの中身を書く • Schedule でジョブを開始 • Complete でジョブ終了確認 • 変数はNativeArray系を使い、自力でDispose

コーディング基本まとめ

• IJob • 1つのスレッドでジョブを回す

• IJobParallelFor • 複数のスレッドでジョブを回す

• IJobParallelForTransform • Transformにアクセスが可能

コーディング基本まとめ

public void Execute() {}

public void Execute(int i) {}

public void Execute(int i, TransformAccess transform){}

Let’s make a mistake!

• マルチスレッドプログラミングは間違えやすい • ちょっとした見落としはしてしまう

• Unityは落ちることなくエラーが教えてくれる • CTO Joachim「Unityは「Sandbox(=砂場)」である」

• 砂場では間違っていい。正解に導いてくれれれば。

エラーまとめ

Let’s try “C# Job コンパイラ”

• 一文付け足すだけ • [ComputeJobOptimizationAttribute(Accuracy

.Med, Support.Relaxed)] • Accuracy は計算の精度

• 新しいmathライブラリ

C# Job Compiler

• float1, float2, float3, float4, • half1, half2, half3, half4 • int1, int2, int3, int4 • math.abs • math.min • math.max • math.pow • math.lerp • math.clamp

• math.saturate • math.select // 条件分岐 • math.rcp // 逆数 • math.sign • math.rsqrt // sqrtの逆数 • math.any • math.all • math.sincos

新mathライブラリ

Let’s implement C# Job System.

public class RotatorOldUpdate : MonoBehaviour { [SerializeField] float m_Speed; public float speed { get { return m_Speed; } set { m_Speed = value; } }

void Update () { transform.rotation = transform.rotation * Quaternion.AngleAxis (m_Speed * Time.deltaTime, Vector3.up); } }

• STEP1:データレイアウトの最適化 • GameObjectごとにするのはやめる • データをシーケンシャルにする

• キャッシュ化する • forループでGetComponentとかしなくてよくなる

Job Component System実装まとめ

public class RotatorOldUpdate : MonoBehaviour { [SerializeField] float m_Speed; public float speed { get { return m_Speed; } set { m_Speed = value; } }

void Update () { transform.rotation = transform.rotation * Quaternion.AngleAxis (m_Speed * Time.deltaTime, Vector3.up); } }

class RotatorManagerMainThread : ScriptBehaviourManager { List<Transform> m_Transforms; NativeList<float> m_Speeds; : protected override void OnUpdate() { base.OnUpdate (); float deltaTime = Time.deltaTime; NativeArray<float> speeds = m_Speeds; for (int i = 0; i != m_Transforms.Count; i++) { var transform = m_Transforms [i]; transform.rotation = transform.rotation * Quaternion.AngleAxis (speeds[i] * deltaTime, Vector3.up); } } : : } public class RotatorWithManagerMainThread : ScriptBehaviour { : (たくさんの実装) : }

• STEP2: Job化 • List<Transform> → TransformAccessArray • IJobParallelForTransform継承したジョブ

• Execute(int index, TransformAccess transform)の実装

Job Component System実装まとめ

class RotatorManagerMainThread : ScriptBehaviourManager { List<Transform> m_Transforms; NativeList<float> m_Speeds; : protected override void OnUpdate() { base.OnUpdate (); float deltaTime = Time.deltaTime; NativeArray<float> speeds = m_Speeds; for (int i = 0; i != m_Transforms.Count; i++) { var transform = m_Transforms [i]; transform.rotation = transform.rotation * Quaternion.AngleAxis (speeds[i] * deltaTime, Vector3.up); } } : : } public class RotatorWithManagerMainThread : ScriptBehaviour { : (たくさんの実装) : }

class RotatorManager : ScriptBehaviourManager { TransformAccessArray m_Transforms; NativeList<float> m_Speeds; JobHandle m_Job; : protected override void OnUpdate() { base.OnUpdate (); m_Job.Complete (); var jobData = new RotatorJob(); jobData.speeds = m_Speeds; jobData.deltaTime = Time.deltaTime; m_Job = jobData.Schedule (m_Transforms); }

struct RotatorJob : IJobParallelForTransform { [ReadOnly] public NativeArray<float> speeds; public float deltaTime; public void Execute(int index, TransformAccess transform) { transform.rotation = transform.rotation * Quaternion.AngleAxis (speeds[index] * deltaTime, Vector3.up); } } }

public class RotatorWithManager : ScriptBehaviour { : (たくさんの実装) : }

• STEP3: データからビヘイビアを分離する • ジョブで使用するデータを分離する • InjectTuplesの導入

• Tuples が付加した配列はindexが同期する • ComponentSystemから継承させる

• マネージャーの仕事を任せる

Job Component System実装まとめ

public class RotationSpeedComponent : ScriptBehaviour { public float speed; }

public class RotatingSystem : ComponentSystem { [InjectTuples] public ComponentArray<Transform> m_Transforms;

[InjectTuples] public ComponentArray<RotationSpeedComponent> m_Rotators;

override protected void OnUpdate() { base.OnUpdate (); float dt = Time.deltaTime; for (int i = 0; i != m_Transforms.Length ;i++) { m_Transforms[i].rotation = m_Transforms[i].rotation * Quaternion.AngleAxis(dt * m_Rotators[i].speed, Vector3.up); } } }

• STEP4: データのstruct化 • MonoBehaviour継承 → IComponentData継承

• struct化 • ComponentSystemからの継承でお手軽マネー

ジャー • ComponentArray → ComponentDataArray

Job Component System実装まとめ

public class RotationSpeedComponent : ScriptBehaviour { public float speed; }

public class RotatingSystem : ComponentSystem { [InjectTuples] public ComponentArray<Transform> m_Transforms;

[InjectTuples] public ComponentArray<RotationSpeedComponent> m_Rotators;

override protected void OnUpdate() { base.OnUpdate (); float dt = Time.deltaTime; for (int i = 0; i != m_Transforms.Length ;i++) { m_Transforms[i].rotation = m_Transforms[i].rotation * Quaternion.AngleAxis(dt * m_Rotators[i].speed, Vector3.up); } } }

[Serializable] public struct RotationSpeed : IComponentData { public float speed; public RotationSpeed (float speed) { this.speed = speed; } }

public class RotationSpeedDataComponent : ComponentDataWrapper<RotationSpeed> { }

public class RotatingDataSystem : ComponentSystem { [InjectTuples] public ComponentArray<Transform> m_Transforms; [InjectTuples] public ComponentDataArray<RotationSpeed> m_Rotators; override protected void OnUpdate() { base.OnUpdate (); float dt = Time.deltaTime; for (int i = 0; i != m_Transforms.Length ;i++) { m_Transforms[i].rotation = m_Transforms[i].rotation * Quaternion.AngleAxis(dt * m_Rotators[i].speed, Vector3.up); } } }

• STEP5: ジョブ実装 と 依存性解決 • IJobParallelForTransformを継承したstruct

• Execute で Transformが使える • ComponentSystem → JobComponentSystem

• GetDependency()で依存性の自動解決

Job Component System実装まとめ

[Serializable] public struct RotationSpeed : IComponentData { public float speed; public RotationSpeed (float speed) { this.speed = speed; } }

public class RotationSpeedDataComponent : ComponentDataWrapper<RotationSpeed> { }

public class RotatingDataSystem : ComponentSystem { [InjectTuples] public ComponentArray<Transform> m_Transforms; [InjectTuples] public ComponentDataArray<RotationSpeed> m_Rotators; override protected void OnUpdate() { base.OnUpdate (); float dt = Time.deltaTime; for (int i = 0; i != m_Transforms.Length ;i++) { m_Transforms[i].rotation = m_Transforms[i].rotation * Quaternion.AngleAxis(dt * m_Rotators[i].speed, Vector3.up); } } }

[Serializable] public struct RotationSpeed : IComponentData { public float speed; public RotationSpeed (float speed) { this.speed = speed; } }

public class RotationSpeedDataComponent : ComponentDataWrapper<RotationSpeed> { }

public class SystemRotator : JobComponentSystem { [InjectTuples] public TransformAccessArray m_Transforms; [InjectTuples] public ComponentDataArray<RotationSpeed> m_Rotators; override protected void OnUpdate() { base.OnUpdate (); var job = new Job(); job.dt = Time.deltaTime; job.rotators = m_Rotators; AddDependency(job.Schedule(m_Transforms, GetDependency ())); } struct Job : IJobParallelForTransform { public float dt; [ReadOnly] public ComponentDataArray<RotationSpeed> rotators; public void Execute(int i, TransformAccess transform) { transform.rotation = transform.rotation * Quaternion.AngleAxis(dt * rotators[i].speed, Vector3.up); } } }

C# Job System 注意点 & まとめ

• データ構造はstructのみ (class はNG) • .NETやUnity のAPIはジョブ内では(基本的に)使えない • 何でもかんでも早くなるわけではない

• 算術系が早くなる、と考えるのが正解 • 相互の距離の計算とか • 敵AIの思考ルーチンとか

C# Job System 注意点

• STEP1 C# Job system • Unity 2017.3 or 2018.X

• STEP2 Component system • STEP3 math library • STEP4 C# Job Compiler

リリース予定

• 多数の敵・味方が出るゲームでの相互距離の計算 • それによるソート

• RTS等で使う影響マップの生成 • 弾幕シューティング • etc…

実際に何に使える?

• マルチスレッドプログラミングが安全に書ける

• 新しいComponent System で簡潔に書ける

• コンパイラをかければさらに早くなる

C# Job System まとめ

Q&A