渋谷ほととぎす通信

「Unityをわかりやすく」初心者のためのゲーム作りブログ

Unity C#JobSystemをとりあえずやってみる序

Unity C#JobSystemをとりあえずやってみる序

こんにちは、Unityエンジニアのオオバです。

そろそろUnityのJobSystemをやらなきゃという思いにかられ、少しずつ始めてみようと思います。

実際JobSystemECSを使わなくてもゲーム自体は作れると思いますが、それらを使うことで、浮いたリソースがクオリティアップにつながるのであれば、やらない手はないかなという思いです。

👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!

準備物

PackageManager経由でJobsパッケージをインストールしておきます。この時Mathematicsパッケージがが必須なので、ついでにインストールしておきます。またあとでBurstCompilerの検証もするためBurstもインストールしておきます。

JobSystem使う上でインポートしているパッケージ一覧(不要なものもある) · GitHub
とりあえず、こんな感じのmanifest.jsonになっています。

IJobインターフェースを使って始める

まずはショートコードから始めるために、公式リファレンスのサンプルを参考にしてみます。
Unity - Scripting API: IJob

いくつかジョブを作る方法があるようですが、もっともシンプルであろうIJobインターフェースを使った方法から始めます。

struct VelocityJob : IJob  
{
    [ReadOnly]  
    public NativeArray velocity;  
    public NativeArray position;  
    public float deltaTime;  
    ///  
    /// ジョブの処理内容  
    ///  
    public void Execute()  
    {
        for (var i = 0; i < position.Length; i++)  
        {
            position[i] = position[i] + velocity[i] * deltaTime;  
        }
    }
}

このように構造体にIJobインターフェースを実装してジョブ(このサンプルではVelocityJob)を定義します。
IJobインターフェースに定義されているのはExecute関数です。Execute内にジョブの処理を書きます。またジョブ実行時に自動で呼ばれます。

バッファにNativeArray型を使用

public NativeArray velocity;  

ジョブ用のバッファはGCを発生させないようにアンマネージドなNativeArray型を使用します。
アンマネージドメモリ領域を確保している変数なので最終的に使用し終えたらDisposeする必要があります。

使い終わったらDispose

velocity.Dispose();  

Unity C#JobSystemをとりあえずやってみる序_0

【Unite Tokyo 2018 Training Day】C#JobSystem & ECSでCPUを極限まで使い倒そう ~C# Jo…

Disposeし忘れた場合、Unity Editor上ではエラーを吐いてくれますが、実機では無視されてしまいメモリリーク状態になってしまうので注意が必要そうです。

Unity C#JobSystemをとりあえずやってみる序_1

プロファイラで確認してみると、WorkerThread(Unityが予め用意しているスレッド)に処理が分散されていることがわかります。

ここまでのソースコードはコチラ
ApplyVelocitySample.cs · GitHub

ここまでのまとめ

  • ジョブ定義にはIJobインターフェースを実装する(他にもインターフェースはあります)
  • NativeArrayでバッファを定義する(NativeArray以外の型もある)
  • Blittable型のみジョブ内では使用可能
  • NativeArrayは使用後はDispose
  • NativeArrayメモリリークエラー出力はUnityエディタ上のみ

ここからは実際にJobSystem使ってみてパフォーマンス的にどうなん?といったところを確認していきます。

早くなっているのか?

先のプロファイラを見てると、

  • Update関数処理時間 : 2.55ms
  • WorkerThreadにおけるジョブの処理時間 : 1.52ms

約60%のリソースがMainThreadからWorkerThreadに移っていることがわかります。
果たしてこれが、どのくらい全体のパフォーマンスアップに繋がっているのか確認したいところです。

ということで、ジョブ使用 / 未使用をフラグで持たせてプロファイラで確認してみます。

Unity C#JobSystemをとりあえずやってみる序_2

結果から見ると、今回のサンプルではあくまでMainThreadからWorkerThreadに処理を分散させる時があり(毎フレームではない)、MainThreadのUpdate内の処理時間自体にあまり変化はありませんでした。

ただしジョブ未使用時はMainThreadをフルで使っていますが、JobSystemを使用することでWorkerThreadに仕事を振ることができた分、端末の高熱化が多少軽減されそうではあります(未検証)。

ここまでのソースコードはコチラ
ジョブの使用/未使用を切り替えてみた · GitHub

ここまでのまとめ

  • 今回のサンプルの場合、JobSystemにおける大きなパフォーマンスアップは無い
  • MainThreadからWorkerThreadに仕事を振ることで、MainThreadがフルで使われない事に意義がありそう

BurstCompilerの力

そういえばJobSystemにはBurstCompilerが使えたな〜ということを思い出したので、検証してみました。
BurstCompilerはJobSystemのExecute内を高速化する特殊なコンパイラです。
BurstCompilerを使うためには、以下のようにJob定義の構造体に[ComputeJobOptimizationAttribute]という属性を記述するだけです。

[ComputeJobOptimizationAttribute]  
struct VelocityJob : IJob  
{
    [ReadOnly]  
    public NativeArray velocity;  
    public NativeArray position;  

BurstCompilerの結果

BurstCompiler未使用状態

Unity C#JobSystemをとりあえずやってみる序_3

BurstCompiler使用状態

Unity C#JobSystemをとりあえずやってみる序_4

という結果でBurstCompilerを使うことで、ジョブの処理が約44倍高速化しました。
これにはびっくり。

もちろんBurstCompilerを使うには制限があって限定的なテクニックかもしれませんが、できるだけ使えるように実装を努力したくなる数値です。

BurstCompierの使用制限

Unity C#JobSystemをとりあえずやってみる序_5

【CEDEC2018】CPUを使い切れ! Entity Component System(通称ECS) が切り開く新しいプログラミング

  • 基本的に差し替え可能なコードで使用可能
  • static変数使用不可

ここまでのまとめ

  • BurstCompilerにはできるだけ対応したい。それだけのメリットがある。

まとめ

IJobインターフェースだけで、長くなってきたので、一旦ここでJobSystemをとりあえずやってみる序はおしまいです。ジョブで処理をさせるには、今までの実装の考え方を180度切り替えて設計する必要がありそうです(クラスが使えないというのは大きい...)。

とりあえず、IJob以外のジョブも一通り触ってみて、適切なジョブの使い方を模索していこうと思います。

今回検証してよかったのは、BurstCompilerはとても高速だということを実感できたことです。
がんばってBurstしていくぞ!

参考

オススメ記事