UnityのC# JobSystemをとりあえず使ってみよう

# 導入
JobSystemを聞いたことがあるだろうか？
聞いたことがなければ一旦Unity公式の以下の動画でどんなものか確認してほしい

[今日から速い！Unity の Burst + C# Job System](https://www.youtube.com/watch?v=rvBpUPFN5_I "今日から速い！Unity の Burst + C# Job System")

この記事では、JobSystemの原理などの詳しい事は置いておいて、とりあえずJobを作って動かしてみる事を体験しよう

## 動作環境
Unity 6000.0.23f1
Burst 1.8.18

# サンプルコード
早速このコンポーネントを実行してみてほしい
コンポーネントをアタッチした状態でゲームを実行するとベンチマークがデバッグログに出力される
またコンテキストメニューの「Benchmark」ボタンからもベンチマークを実行できる

※同期処理のため、_arrayRangeを大きくし過ぎるとフリーズするので注意
```csharp
using System.Diagnostics;
using System.Linq;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;

public class JobSystemExample : MonoBehaviour
{
    [SerializeField]
    private int _arrayRange = 100000;

    private void Start()
    {
        JobSytemBenchmark();
    }

    [ContextMenu("Benchmark")]
    private void JobSytemBenchmark()
    {
        long normalWatch = 0;
        long job1Watch = 0;
        long job2Watch = 0;
        long job3Watch = 0;
        long job4Watch = 0;

        int[] numbers = Enumerable.Range(0, _arrayRange).ToArray();
        bool[] result = new bool[numbers.Length];
        
        //通常の処理
        Stopwatch stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < numbers.Length; i++)
        {
            result[i] = IsPrime(numbers[i]);
        }
        stopwatch.Stop();
        normalWatch = stopwatch.ElapsedMilliseconds;

        // NativeArrayを使用してジョブの結果を格納
        using (NativeArray<int> numbersArray = new(numbers, Allocator.TempJob))
        {
            using (NativeArray<bool> resultArray = new NativeArray<bool>(numbers.Length, Allocator.TempJob))
            {
                stopwatch.Restart();
                //ジョブ1
                var job1 = new JobOne() { numbers = numbersArray, results = resultArray };
                var handle1 = job1.Schedule();

                handle1.Complete();
                stopwatch.Stop();
                job1Watch = stopwatch.ElapsedMilliseconds;

                stopwatch.Restart();

                //ジョブ2
                var job2 = new JobTwo() { numbers = numbersArray, results = resultArray };
                var handle2 = job2.Schedule();

                handle2.Complete();
                stopwatch.Stop();
                job2Watch = stopwatch.ElapsedMilliseconds;

                stopwatch.Restart();

                //ジョブ3
                var job3 = new JobThree() { numbers = numbersArray, results = resultArray };
                var handle3 = job3.Schedule(numbersArray.Length, 0);

                handle3.Complete();
                stopwatch.Stop();
                job3Watch = stopwatch.ElapsedMilliseconds;

                stopwatch.Restart();
                //ジョブ4
                var job4 = new JobFour() { numbers = numbersArray, results = resultArray };
                var handle4 = job4.Schedule(numbersArray.Length, 0);

                handle4.Complete();
                stopwatch.Stop();
                job4Watch = stopwatch.ElapsedMilliseconds;

                UnityEngine.Debug.Log($"Array Range is <color=yellow>{_arrayRange}</color>\n" +
                    $"normal : <b>{normalWatch}</b> ms\n" +
                    $"IJob : <b>{job1Watch}</b> ms\n" +
                    $"IJob Burst : <b>{job2Watch}</b> ms\n" +
                    $"IJobParallelFor : <b>{job3Watch}</b> ms\n" +
                    $"IJobParallelFor Burst : <b>{job4Watch}</b> ms");
            }
        }
    }

    static private bool IsPrime(int number)
    {
        if (number < 2) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        for (int i = 3; i * i <= number; i += 2)
        {
            if (number % i == 0) return false;
        }
        return true;
    }

    private struct JobOne : IJob
    {
        public NativeArray<int> numbers;
        public NativeArray<bool> results;

        public void Execute()
        {
            for (int i = 0; i < numbers.Length; i++)
                results[i] = IsPrime(numbers[i]);
        }

        static private bool IsPrime(int number)
        {
            if (number < 2) return false;
            if (number == 2) return true;
            if (number % 2 == 0) return false;

            for (int i = 3; i * i <= number; i += 2)
            {
                if (number % i == 0) return false;
            }
            return true;
        }
    }

    [BurstCompile]
    private struct JobTwo : IJob
    {
        public NativeArray<int> numbers;
        public NativeArray<bool> results;

        public void Execute()
        {
            for (int i = 0; i < numbers.Length; i++)
                results[i] = IsPrime(numbers[i]);
        }

        static private bool IsPrime(int number)
        {
            if (number < 2) return false;
            if (number == 2) return true;
            if (number % 2 == 0) return false;

            for (int i = 3; i * i <= number; i += 2)
            {
                if (number % i == 0) return false;
            }
            return true;
        }
    }

    private struct JobThree : IJobParallelFor
    {
        public NativeArray<int> numbers;
        public NativeArray<bool> results;

        public void Execute(int index)
        {
            results[index] = IsPrime(numbers[index]);
        }

        static private bool IsPrime(int number)
        {
            if (number < 2) return false;
            if (number == 2) return true;
            if (number % 2 == 0) return false;

            for (int i = 3; i * i <= number; i += 2)
            {
                if (number % i == 0) return false;
            }
            return true;
        }
    }

    [BurstCompile]
    private struct JobFour : IJobParallelFor
    {
        public NativeArray<int> numbers;
        public NativeArray<bool> results;

        public void Execute(int index)
        {
            results[index] = IsPrime(numbers[index]);
        }

        static private bool IsPrime(int number)
        {
            if (number < 2) return false;
            if (number == 2) return true;
            if (number % 2 == 0) return false;

            for (int i = 3; i * i <= number; i += 2)
            {
                if (number % i == 0) return false;
            }
            return true;
        }
    } 
}

```
## サンプルコードの解説
このコンポーネントは_arrayRange個の数値の配列を素数判定する時間を計測している
今回の例では、1000000個の素数判定を実行した処理速度を測っている

### 通常の処理
IsPrimeメソッドに数値の配列の要素一つ一つを渡して実行し、素数判定の結果をresultに記録している
これは馴染みのあるfor文を用いた繰り返し処理で、実行したところ私の環境では72msほどの時間がかかった
```cs
for (int i = 0; i < numbers.Length; i++)
{
    result[i] = IsPrime(numbers[i]);
}
```


### ジョブ１
JobSystemを用いて上記と通常の処理と同じことを実行する
コードの説明を省いて行っている事だけを説明すると、ジョブに数値の配列を渡して実行を開始し、処理が終了するまで待機している
これはJobSystemを使用しているが、わざわざ通常の処理と同じ事をするためにジョブに配列を渡しているため、むしろ時間がかかってしまう
上記と同じく1000000個の素数判定をしたところ、124msほど掛かってしまった

```csharp
var job1 = new JobOne() { numbers = numbersArray, results = resultArray };
var handle1 = job1.Schedule();

handle1.Complete();
```
```csharp
private struct JobOne : IJob
{
    public NativeArray<int> numbers;
    public NativeArray<bool> results;

    public void Execute()
    {
        for (int i = 0; i < numbers.Length; i++)
            results[i] = IsPrime(numbers[i]);
    }

    static private bool IsPrime(int number)
    {
        if (number < 2) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        for (int i = 3; i * i <= number; i += 2)
        {
            if (number % i == 0) return false;
        }
        return true;
    }
}
```

### ジョブ２
JobSystemにBurstCompileを適用して、処理を高速化する
BurstCompileとは、とりあえず処理を高速化する機能という認識を持っておこう
適用すると先ほどまでの3倍の速度、45msほどで処理が完了する

```csharp
var job2 = new JobTwo() { numbers = numbersArray, results = resultArray };
var handle2 = job2.Schedule();

handle2.Complete();
```
```csharp
[BurstCompile]
private struct JobTwo : IJob
{
    public NativeArray<int> numbers;
    public NativeArray<bool> results;

    public void Execute()
    {
        for (int i = 0; i < numbers.Length; i++)
            results[i] = IsPrime(numbers[i]);
    }

    static private bool IsPrime(int number)
    {
        if (number < 2) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        for (int i = 3; i * i <= number; i += 2)
        {
            if (number % i == 0) return false;
        }
        return true;
    }
}
```

### ジョブ３
先ほどまでは一つのワーカー（仕事人）が配列を一つづつ処理していたが、今回はワーカーを複数使って、同時並行で処理をしている
なので単純に考えて、ワーカーの数だけ処理速度が倍になる
実行してみると30msほどで処理が完了した
```csharp
var job3 = new JobThree() { numbers = numbersArray, results = resultArray };
var handle3 = job3.Schedule(numbersArray.Length, 0);

handle3.Complete();
```

```csharp
private struct JobThree : IJobParallelFor
{
    public NativeArray<int> numbers;
    public NativeArray<bool> results;

    public void Execute(int index)
    {
        results[index] = IsPrime(numbers[index]);
    }

    static private bool IsPrime(int number)
    {
        if (number < 2) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        for (int i = 3; i * i <= number; i += 2)
        {
            if (number % i == 0) return false;
        }
        return true;
    }
}
```

### ジョブ４
ジョブ３にBurstを適用したもの
同時並行で処理を行っている上に、Burstによって処理速度が上がっているため、最高速で処理が行われる
実行時間は脅威の9msである
```csharp
var job4 = new JobFour() { numbers = numbersArray, results = resultArray };
var handle4 = job4.Schedule(numbersArray.Length, 0);

handle4.Complete();
```

```csharp
[BurstCompile]
private struct JobFour : IJobParallelFor
{
    public NativeArray<int> numbers;
    public NativeArray<bool> results;

    public void Execute(int index)
    {
        results[index] = IsPrime(numbers[index]);
    }

    static private bool IsPrime(int number)
    {
        if (number < 2) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        for (int i = 3; i * i <= number; i += 2)
        {
            if (number % i == 0) return false;
        }
        return true;
    }
} 
```

ここまで見てきて、コードの内容は良く分からなかったと思うが、少なくともジョブ４の高速性を理解してくれたと思う
ということで、ジョブ４のIJobParallelForとBurstCompileについての使い方のみを解説する

# ジョブの使い方
## 制約
今回のJobSystemとBurstCompileを使用するには、以下の制約をクリアしなければならない
>C#前提。C#のポインタは使える。
仮想メソッドを持つことはできない。
GCも使えない。
参照型は無し。プリミティブ型と構造体のみ。
NativeArray構造体である。
連続したメモリレイアウト前提。
データの入出力がはっきりしてること。
SIMD auto -vectorization
Preciton control (Low, Med, High)
[参考 - Burstの前提条件とは？](https://qiita.com/4_mio_11/items/cda0c8aa4c77a7748418 "Unity開発するにあたって知っておきたいコンパイラのすゝめ(Part3～Burst編～)")

## ジョブの作り方
ジョブはstructとして作る
「IJobParallelFor」というinterfaceを継承する
このインターフェースは「Execute(int index)」というメソッドを実装する
Executeメソッドの引数はジョブの番号で、例えば32個のジョブを並行処理するときは、引数に0~31が渡され32回実行される
```csharp
private struct ExampleJob : IJobParallelFor
{
    public void Execute(int index)
    {
        //ここにジョブの処理を実装する
    }
}
```
## ジョブの実行方法
まず先ほど作ったジョブのstructをインスタンス化させる
次にインスタンスにScheduleメソッドを使ってジョブの処理を始める
Completeでジョブの終了を待機する
```csharp
var ExampleJob = new ExampleJob();
var handle = ExampleJob.Schedule(32, 0);

handle.Complete();
```
## ジョブに配列を渡して処理する
もしサンプルコードのように配列の要素に一気に処理を走らせたい時は「NativeArray」を使用する
```csharp
private struct ExampleJob : IJobParallelFor
{
    public NativeArray<int> numbers;
    
    public void Execute(int index)
    {
        //ここにジョブの処理を実装する
        numbers[index] += 1;
    }
}
```
NativeArrayは特殊な配列で、usingステートメントを使用する
Scheduleメソッドの第一引数はジョブの数なので、配列のLengthを指定すると良い
```csharp
int[] numbersArray = new int[5] { 1, 2, 3, 4, 5 };

using (NativeArray<int> numbersNativeArray = new(numbersArray, Allocator.TempJob))
{
    var ExampleJob = new ExampleJob() { numbers = numbersNativeArray };
    var handle = ExampleJob.Schedule(numbersNativeArray.Length, 0);

    handle.Complete();
}
```

## BurstCompileを適用する
ジョブの上にBurstCompileアトリビュートを追加するだけ
```csharp
[BurstCompile]
private struct ExampleJob : IJobParallelFor
{
    public NativeArray<int> numbers;
    
    public void Execute(int index)
    {
        //ここにジョブの処理を実装する
        numbers[index] += 1;
    }
}
```
# 終わり
このような手順でIJobParallelForとBurstCompileを使った高速な並列処理が行える
説明を端折って例を多めにしたので、もし説明不足があれば追記しようと思う