はじめに
今回はJobSystem
を始めて使ってみようという記事になります。
ただ公式ドキュメントもしっかり日本語対応していて、割と読みやすいと思うのでより理解を深めたい場合は是非見てみるとよいでしょう。
始め方
手法が二つあるのでお好きな方でできます。
- manifest.jsonに追記する
- PackageManagerでGitからインストールする
manifest.jsonに追記
"com.unity.jobs": "0.11.0-preview.6"
またバージョンに関しては現時点(2022/1/1)で一番新しいものを入れました。
PackageManagerを利用
com.unity.jobs
並列処理する 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の箇所)
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
ではまた。