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 write A コードが汚い 可読性が低い 間違っても気づきにくい? B write A B read A
MTP のここが嫌だその 2 デッドロックが嫌だ 面倒臭い コードが汚い 可読性が低い 間違うと無限ループ B 待ち 待ち A B A
MTP のここが嫌だその 3 難解なところが嫌だ mutex:lock とか アトミック変数とか
MTP のここが嫌だその 4 デバッグが嫌だ 正常に 動いてしまったり する 無限ループになったりする 突然ハングアップしたりする リリース後にバグが判明したりする
C# Job System の概要
116
116 Boid シミュレーションをマルチスレッドで 8 core CPU で動かした場合の速度倍
Demo
C# Job System の特徴 簡潔に書ける GCフリー 安全 高速な新コンパイラ
特徴 1 簡潔に書ける Data Oriented Programming データとビヘイビア ( 振舞い ) の分離 struct( 構造体 ) コンポーネントの導入 Job Component System の用意 簡潔に書けるようにマネージャーを用意
特徴 2 GC フリー GCをいかにさせないか NativeArrayの導入 以下の感じで確保 var src = new NativeArray<float>(500, Allocator.Temp); 以下の感じで解放 ( 自分で ) src.dispose(); 要素数アロケーターの種類
特徴 2 GC フリー 他の NativeArray ファミリー struct NativeArray<Value> // struct NativeList<Value> // struct NativeSlice<Value> // struct NativeHashmap<Key, Value> // Dictionary struct NativeMultiHashmap<Key, Value> // Dictionary
特徴 3 安全 エラーで指摘してくれる 落ちることはない レースコンディション デッドロックは起こり得ない Sandbox
特徴 4 高速な新コンパイラ C# [Mono] IL [C# Job Compiler] 内部的な Domain Model [ 最適化 ] [LLVM] 実行形式 10 倍 20 倍高速になる 電池消費の軽減 Why faster? SIMD 命令の有効利用 正確さとパフォーマンスのトレードオフ
Let s read C# Job System codes!
コーディング基本まとめ IJob~ でジョブを定義 Execute にジョブの中身を書く Schedule でジョブを開始 Complete でジョブ終了確認 変数はNativeArray 系を使い 自力でDispose
コーディング基本まとめ IJob 1つのスレッドでジョブを回す public void Execute() IJobParallelFor 複数のスレッドでジョブを回す public void Execute(int i) IJobParallelForTransform Transformにアクセスが可能 public void Execute(int i, TransformAccess transform)
Let s make a mistake!
エラーまとめ マルチスレッドプログラミングは間違えやすい ちょっとした見落としはしてしまう Unityは落ちることなくエラーが教えてくれる CTO Joachim Unityは Sandbox(= 砂場 ) である 砂場では間違っていい 正解に導いてくれれれば
Let s try C# Job コンパイラ
C# Job Compiler 一文付け足すだけ [ComputeJobOptimizationAttribute(Accuracy.Med, Support.Relaxed)] Accuracy は計算の精度 新しいmathライブラリ
新 math ライブラリ 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
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);
Job Component System 実装まとめ STEP1: データレイアウトの最適化 GameObjectごとにするのはやめる データをシーケンシャルにする キャッシュ化する forループでgetcomponentとかしなくてよくなる
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 : ( ) :
Job Component System 実装まとめ STEP2: Job 化 List<Transform> TransformAccessArray IJobParallelForTransform 継承したジョブ Execute(int index, TransformAccess transform) の実装
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 : ( ) :
Job Component System 実装まとめ STEP3: データからビヘイビアを分離する ジョブで使用するデータを分離する InjectTuplesの導入 Tuples が付加した配列はindexが同期する ComponentSystemから継承させる マネージャーの仕事を任せる
public class RotationSpeedComponent : ScriptBehaviour public float speed; public class RotatingSystem : ComponentSystem [InjectTuples] public ComponentArray<Transform> [InjectTuples] public ComponentArray<RotationSpeedComponent> m_transforms; 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);
Job Component System 実装まとめ STEP4: データのstruct 化 MonoBehaviour 継承 IComponentData 継承 struct 化 ComponentSystemからの継承でお手軽マネージャー ComponentArray ComponentDataArray
public class RotationSpeedComponent : ScriptBehaviour public float speed; public class RotatingSystem : ComponentSystem [InjectTuples] public ComponentArray<Transform> [InjectTuples] public ComponentArray<RotationSpeedComponent> m_transforms; 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);
Job Component System 実装まとめ STEP5: ジョブ実装と依存性解決 IJobParallelForTransformを継承したstruct Execute で Transformが使える ComponentSystem JobComponentSystem GetDependency() で依存性の自動解決
[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 注意点 & まとめ
C# Job System 注意点 データ構造はstructのみ (class はNG).NETやUnity のAPIはジョブ内では ( 基本的に ) 使えない 何でもかんでも早くなるわけではない 算術系が早くなる と考えるのが正解 相互の距離の計算とか 敵 AIの思考ルーチンとか
リリース予定 STEP1 C# Job system Unity 2017.3 or 2018.X STEP2 Component system STEP3 math library STEP4 C# Job Compiler
C# Job System まとめ マルチスレッドプログラミングが安全に書ける 新しい Component System で簡潔に書ける コンパイラをかければさらに早くなる
Q&A