C# Job Systemざっくりまとめ
前書き
DOTS の1つである C# Job System について軽くまとめました。
ざっくりとした説明なので詳しい情報は調べてみてください。
なに?
Unityエンジンの並列処理システム。
恩恵
- メモリ使用量が減る。
- 並列化が容易。
- メモリレイアウトが良くなりキャッシュ効率が上がる。
どういうことをしてるのか
Unityが管理しているワーカースレッドに登録されたJobを実行する。
特徴
- オーバーヘッドが非常に低い(主にIL2CPP想定)
- Burstコンパイラ(C# Job System専用コンパイラ)向けに最適化される
- すでに動いているスレッドにジョブを発行するのでコンテキストスイッチの頻度が減らせる。
制約
- Blittable型しか扱えない
- Blittable型についてはこの記事が参考になります。
【Unity】UnsafeUtilityについて纏めてみる - Qiita
- Blittable型についてはこの記事が参考になります。
- プログラマ側で細かく制御できないこと
- データのロックができない
- Jobのタイミングの同期ができない
- Job同士でのデータの受け渡しができない
種類
- IJob
- ワーカースレッド1つで実行される
- IJobParallelFor
- 複数のワーカースレッドで実行される(並列化)
基本的な実装・処理の流れ
- Jobにしたいのstructにジョブのインターフェイスを継承させる。
- struct内にExecute()関数を定義する。
- IJob.Schedule()でジョブを登録する。
- IJob.Schedule()で登録したstructのExecuteが実行される。
- JobHandle.Complete()で完了待ちする。
IJobについて
実行されるジョブが1つのワーカースレッドで処理される。
使い方
using UnityEngine; using Unity.Collections; using Unity.Jobs; class Sample_IJob : MonoBehaviour { // ジョブ定義 struct VelocityJob : IJob { [ReadOnly] public NativeArray<Vector3> velocity; public NativeArray<Vector3> position; public float deltaTime; // Executeを定義 public void Execute() { for(var i = 0; i < position.Length; ++i) position[i] = position[i] + velocity[i] * deltaTime; } } public void Update() { var position = new NativeArray<Vector3>(500, Allocator.Persistent) var velocity = new NativeArray<Vector3>(500, Allocator.Persistent); for(var i = 0; i < velocity.Length ++i) velocity[i] = new ector3(0, 10, 0); // ジョブ前処理 var job = new VelocityJob() { deltaTime = Time.deltaTime, position = position, velocity = velocity }; // ジョブをスケジュール JobHandle = jobHandle = job.Schedule(); // ジョブ完了待ち jobHandle.Complete(); Debug.Log(job.position[0]); position.Dispose(); velocity.Dispose(); } }
IJobParallelForについて
NativeContainer の各要素、または一定の反復回数に対して、同じ独立した操作を実行できる。
スケジュールされるとジョブの Execute(int index) メソッドが複数のワーカースレッドで平行して呼び出される。
引数に渡ってくるインデックスは要素の添え字なのでこれを使用する。
Unity は batchSize 以上のチャンクに作業を自動的に分割し、ワーカースレッドの数、配列の長さ、バッチサイズに基づいて適切な数のジョブをスケジュールする。
バッチサイズの選定は、単純な処理(Vector3 の足し算のみ等)だと32~128が良い。複雑な処理は小さいバッチサイズがオススメ。ただし、スケジューリングにもオーバーヘッドがあるのでタダではない。
IJob と違って Execute 関数の引数にインデックスが渡ってくるので、それを使って要素の処理をする。
使い方
using UnityEngine; using Unity.Collections; using Unity.Jobs; class Sample_IJobParallelFor : MonoBehaviour { // ジョブを定義 struct VelocityJob : IJobParallelFor { [ReadOnly] public NativeArray<Vector3> velocity; public NativeArray<Vector3> position; public float deltaTime; // Executeを定義 public void Execute(int i) { position[i] = position[i] + velocity[i] * deltaTime; } } void Update() { var position = new NativeArray<Vector3>(500, Allocator.Persistent); var velocity = new NativeArray<Vector3>(500, Allocator.Persistent); for(var i = 0; i < velocity.Length; ++i) velocity[i] = new Vector3(0, 10, 0); // ジョブ前処理 var job = new VelocityJob() { deltaTime = Time.deltaTime, position = position, velocity = velocity } // ジョブをスケジュール JobHandle jobHandle = job.Schedule(position.Length, 64); // ジョブ完了待ち jobHandle.Complete(); Debug.Log(job.position[i]); position.Dispose(); velocity.Dispose(); } }