渋谷ほととぎす通信

完全趣味でやってるUnityメモ。説明できないところを説明できるようにするための個人ブログ。昨日の自分より少しでも大きくなれるように。。。 ※所属団体とは一切関係がありません

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


f:id:esakun:20150730215258g:plain

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

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

準備物 

PackageManager経由でJobsパッケージをインストールしておきます。この時Mathematicsパッケージがが必須なので、ついでにインストールしておきます。またあとでBurstCompilerの検証もするためBurstもインストールしておきます。
※Mathematicsがないとコンパイルエラーが起きます とりあえず、こんな感じのmanifest.jsonになっています。
※今回のJobSystemを使うにあたっては不要なものも結構はいっているので参考程度で

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

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

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

struct VelocityJob : IJob
{
    [ReadOnly]
    public NativeArray<Vector3> velocity;
    public NativeArray<Vector3> position;
    public float deltaTime;
    /// <summary>
    /// ジョブの処理内容
    /// </summary>
    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<Vector3> velocity;

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

使い終わったらDispose

velocity.Dispose();

f:id:esakun:20180909235317p:plain:w400
【Unite Tokyo 2018 Training Day】C#JobSystem & ECSでCPUを極限まで使い倒そう ~C# Jo…P15より

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

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

ここまでのソースコードはコチラ

ここまでのまとめ

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

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

早くなっているのか?

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

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

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

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

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

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

ここまでのソースコードはコチラ

ここまでのまとめ

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

BurstCompilerの力

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

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

BurstCompilerの結果

BurstCompiler未使用状態
f:id:esakun:20180910011248p:plain:w300

BurstCompiler使用状態
f:id:esakun:20180910011337p:plain:w300

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

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

BurstCompierの使用制限

f:id:esakun:20180910014825p:plain:w500
【CEDEC2018】CPUを使い切れ! Entity Component System(通称ECS) が切り開く新しいプログラミングP50より

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

ここまでのまとめ

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

まとめ

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

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

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

参考