はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】JobSystemを初めて触ってみる

はじめに

今回はJobSystemを始めて使ってみようという記事になります。

docs.unity3d.com

ただ公式ドキュメントもしっかり日本語対応していて、割と読みやすいと思うのでより理解を深めたい場合は是非見てみるとよいでしょう。

その前に

この記事ではJobSystemの利用方法について触れますが、仕組みについては全く触れません。

詳しくは公式ドキュメント等を参照してみてください。
docs.unity3d.com

tsubakit1.hateblo.jp

始め方

手法が二つあるのでお好きな方でできます。

  • manifest.jsonに追記する
  • PackageManagerでGitからインストールする

manifest.jsonに追記

"com.unity.jobs": "0.11.0-preview.6"
f:id:hanaaaaaachiru:20220104230853p:plain
manifest.jsonに追加で記述する

またバージョンに関しては現時点(2022/1/1)で一番新しいものを入れました。

PackageManagerを利用

com.unity.jobs
f:id:hanaaaaaachiru:20220104231024p:plain
Package Managerからインストールする

並列処理する 1 つのジョブをスケジュールする

まずは一つのジョブをWorkerThreadに割り当てるサンプル。

public class JobSystemTest : MonoBehaviour
{
    private struct MyJob : IJob
    {
        // Blittableデータ型はScheduleメソッド呼び出しの際にコピーされる
        public float a;
        public float b;
        
        // NativeContainer(NativeArray, NativeList, NativeHashMap, NativeMultiHashMap, NativeQueue)はコピーされずメインスレッド上の共有データにアクセスする
        public NativeArray<float> result;
        
        // 1つのコアで一回実行される
        public void Execute()
        {
            result[0] = a + b;
        }
    }

    private void Update()
    {
        // Jobで利用するNativeContainerを生成する
        // NativeContainer生成とき必要なメモリ割り当てのタイプを指定する必要がある
        // Allocator.Temp : 1フレーム以下で破棄, Allocator.TempJob : 4フレーム以内で破棄, Allocator.Persistent : 制限なし
        var resultArray = new NativeArray<float>(1, Allocator.TempJob);

        // メンバ変数をセットしながらJobを生成する
        // Blittableデータ型(float等)はデータをコピー,NativeContainerは参照を渡す
        var myJob = new MyJob
        {
            a = 5,
            b = 10,
            result = resultArray,
        };

        // ジョブをスケジュールする(この時点でJobが実行されているわけではない)
        var handle = myJob.Schedule();

        // ジョブを実行する (JobHandle.ScheduleBatchedJobsメソッドを呼ぶか、Completeを呼ぶとジョブが実行される)
        JobHandle.ScheduleBatchedJobs();

        // メインスレッドでは別の処理をさせたり
        // Completeを呼ぶとメインスレッドがJobの完了待ちになるので、上手く活用したい
        Thread.Sleep(10);
        
        // ジョブが完了するのを待つ(JobHandle.ScheduleBatchedJobsを実行しないと、Complete呼び出し時にJobの実行が開始される)
        handle.Complete();
        
        Debug.Log($"resultArray[0] = {resultArray[0]}");

        // NativeContainerの破棄
        resultArray.Dispose();
    }
}

複数のコアで並列実行

次は複数のWorkerThreadにジョブを割り当てるサンプル。

public class JobSystemTest : MonoBehaviour
{
    private struct MyParallelJob : IJobParallelFor
    {
        [ReadOnly] public NativeArray<float> a;
        [ReadOnly] public NativeArray<float> b;

        public NativeArray<float> result;
        
        public void Execute(int index)
        {
            result[index] = a[index] + b[index];
        }
    }
    
    private void Update()
    {
        var count = 2;
        var a = new NativeArray<float>(count, Allocator.TempJob);
        var b = new NativeArray<float>(count, Allocator.TempJob);
        var result = new NativeArray<float>(count, Allocator.TempJob);
        
        a[0] = 1.1f;
        b[0] = 2.2f;
        a[1] = 3.3f;
        b[1] = 4.4f;

        // Job生成&データ設定
        var job = new MyParallelJob
        {
            a = a,
            b = b,
            result = result
        };

        // Jobを並列実行するようにスケジュール
        // 第一引数 arrayLength : The number of iterations the for loop will execute.
        // 第二引数 innerloopBatchCount : Granularity in which workstealing is performed. A value of 32, means the job queue will steal 32 iterations and then perform them in an efficient inner loop.
        var handle = job.Schedule(result.Length, 1);
        
        // ジョブを開始する(メインスレッドで処理待ちだけする場合はこれを書かずにCompleteでよい)
        JobHandle.ScheduleBatchedJobs();
        
        handle.Complete();

        Debug.Log($"{result[0]} , {result[1]}");
        
        a.Dispose();
        b.Dispose();
        result.Dispose();
    }
}


job.Scheduleの引数の箇所が分かりにくいと思いますが、公式ドキュメントには以下のように記載されていました。

引数 意味
arrayLength The number of iterations the for loop will execute.
innerloopBatchCount Granularity in which workstealing is performed. A value of 32, means the job queue will steal 32 iterations and then perform them in an efficient inner loop.

Unity.Jobs.IJobParallelForExtensions-Schedule - Unity スクリプトリファレンス

英語でよく分かりませんが、以下のような感じだと思います。

  • arrayLength : Executeメソッドが実行される回数
  • innerloopBatchCount : バッチ処理にいくつの要素を持つジョブをスケジュールするか(Batch数)

innerloopBatchCountはジョブのスケジュールをする際にどれくらいの粒度で振り分けを行うかという意味だと私は解釈しています。(図の左から2番目のBatchの箇所)

f:id:hanaaaaaachiru:20220105002003p:plain
ParallelFor ジョブのスケジュール

docs.unity3d.com

innerloopBatchCountが小さいと細かく割り振りを行うのに対し、大きいとボンボンと割り振っていくイメージでしょうか。

一応どれくらいの値を指定してあげれば良いかの説明が公式ドキュメントに記載されていました。

Batch size should generally be chosen depending on the amount of work performed in the job. A simple job, for example adding a couple of Vector3 to each other should probably have a batch size of 32 to 128. However if the work performed is very expensive then it is best to use a small batch size, for expensive work a batch size of 1 is totally fine. IJobParallelFor performs work stealing using atomic operations. Batch sizes can be small but they are not for free.

Unity - Scripting API: IJobParallelFor

雑に訳すと単純なジョブなら32~128ぐらいで、複雑なジョブなら1とかで良いといった感じです。

Jobの依存関係

public class JobSystemTest : MonoBehaviour
{
    private struct MyJob1 : IJob
    {
        public NativeArray<float> values;
        
        public void Execute()
        {
            values[0] = values[0] + 1;
        }
    }

    private struct MyJob2 : IJob
    {
        public NativeArray<float> values;

        public void Execute()
        {
            values[0] = values[0] * values[0];
        }
    }
    
    private void Update()
    {
        var result = new NativeArray<float>(1, Allocator.TempJob);
        result[0] = 1;

        var myJob1 = new MyJob1
        {
            values = result
        };

        var myJob2 = new MyJob2
        {
            values = result,
        };
        
        // 必ずmyJob1 -> myJob2の順番で実行する
        var firstJobHandle = myJob1.Schedule();
        var handle = myJob2.Schedule(firstJobHandle);

        handle.Complete();
        
        // 4
        Debug.Log(result[0]);

        result.Dispose();
    }
}

さいごに

また別の記事でTransformを扱うことができるようになるParallelForTransformについても記載できたらなと思います。

加えて公式のヒント・トラブルシューティングというページがあるのですが、JobSystemを利用するにあたって一読の価値はあるので是非。
docs.unity3d.com

ではまた。