2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-01-22

導入

JobSystemを聞いたことありますか?
聞いたことがなければUnity公式の以下の動画がおススメです!

今日から速い!Unity の Burst + C# Job System

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

動作環境

Unity 6000.0.23f1
Burst 1.8.18

サンプルコード

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

※同期処理のため、_arrayRangeを大きくし過ぎるとフリーズするので注意

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ほどの時間が掛かりました。

for (int i = 0; i < numbers.Length; i++)
{
    result[i] = IsPrime(numbers[i]);
}

ジョブ1

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

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

handle1.Complete();
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;
    }
}

ジョブ2

JobSystemにBurstCompileを適用して、処理を高速化します。
BurstCompileとは、出来る事が少ない代わりに高速に処理ができるようにする機能みたいなものです。
適用すると先ほどまでの3倍の速度、45msほどで処理が完了します。

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

handle2.Complete();
[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;
    }
}

ジョブ3

先ほどまでは一つのワーカー(仕事人)が配列を一つづつ処理していましたが、今回はワーカーを複数使って、同時並行で処理をしています。
なので単純に考えて、ワーカーの数だけ処理速度が倍になるという事です。
実行してみると30msほどで処理が完了しました。

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

handle3.Complete();
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;
    }
}

ジョブ4

ジョブ3にBurstを適用したものです。
同時並行で処理を行っている上に、Burstによって処理速度が上がっているため、最高速で処理が行われる
実行時間は脅威の9msで完了してしまいました。

var job4 = new JobFour() { numbers = numbersArray, results = resultArray };
var handle4 = job4.Schedule(numbersArray.Length, 0);

handle4.Complete();
[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;
    }
} 

ここまで見て頂いて、少なくともジョブ4の高速性を理解したと思います。
ということで、ジョブ4のIJobParallelForとBurstCompileについての使い方のみを解説します。

ジョブの使い方

制約

今回のJobSystemとBurstCompileを使用するには、以下の制約をクリアしなければいけません。

C#前提。C#のポインタは使える。
仮想メソッドを持つことはできない。
GCも使えない。
参照型は無し。プリミティブ型と構造体のみ。
NativeArray構造体である。
連続したメモリレイアウト前提。
データの入出力がはっきりしてること。
SIMD auto -vectorization
Preciton control (Low, Med, High)
参考 - Burstの前提条件とは?

ジョブの作り方

ジョブはstructとして作ります。
「IJobParallelFor」というinterfaceを継承します。
このインターフェースは「Execute(int index)」というメソッドを実装する必要があります。
Executeメソッドの引数はジョブの番号で、例えば32個のジョブを並行処理するときは、引数に0~31が渡され32回実行されます。

private struct ExampleJob : IJobParallelFor
{
    public void Execute(int index)
    {
        //ここにジョブの処理を実装する
    }
}

ジョブの実行方法

まず先ほど作ったジョブのstructをインスタンス化させます。
次にインスタンスにScheduleメソッドを使ってジョブの処理を始めます。
Completeでジョブの終了を待機します。

var ExampleJob = new ExampleJob();
var handle = ExampleJob.Schedule(32, 0);

handle.Complete();

ジョブに配列を渡して処理する

もしサンプルコードのように配列の要素に一気に処理を走らせたい時は「NativeArray」を使用します。

private struct ExampleJob : IJobParallelFor
{
    public NativeArray<int> numbers;
    
    public void Execute(int index)
    {
        //ここにジョブの処理を実装する
        numbers[index] += 1;
    }
}

NativeArrayは特殊な配列で、Disposeが必要なので、安全のためにusingステートメント使用をおススメします。
Scheduleメソッドの第一引数はジョブの数なので、配列のLengthを指定しています。

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アトリビュートを追加するだけです。
その処理がBurst化できるのかはよく確認してください!

[BurstCompile]
private struct ExampleJob : IJobParallelFor
{
    public NativeArray<int> numbers;
    
    public void Execute(int index)
    {
        //ここにジョブの処理を実装する
        numbers[index] += 1;
    }
}

終わり

このような手順でIJobParallelForとBurstCompileを使った高速な並列処理が行えます。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?