C# Job Systemざっくりまとめ

前書き

DOTS の1つである C# Job System について軽くまとめました。
ざっくりとした説明なので詳しい情報は調べてみてください。


注意点

2019/12/01 時点で調べた内容です。

ECSやBurst Compilerを併せて使ったときの説明が含まれており、ピュアな C# Job System とは違うので気を付けてください。


なに?

Unityエンジンの並列処理システム。



恩恵

  • メモリ使用量が減る。
  • 並列化が容易。
  • メモリレイアウトが良くなりキャッシュ効率が上がる。



どういうことをしてるのか

Unityが管理しているワーカースレッドに登録された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();
    }
}



最後に

C# Job Systemを簡単にですが、まとめてみました。
アンマネージドなメモリを使うのでいろいろ問題が起きると思いますが、使い方自体はシンプルで良いですね。