徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】アセット読書会に行ってきたよ。NativeArrayってなんだろう?

 京都で行われるUnity技術者の集いである京ゆににて、アセット読書会なる会が開かれました。

kyotounity.doorkeeper.jp

 各自好き勝手Unityを触っているもくもく会とは違い、Unityの特定の技術を一緒に勉強していこうよ、という会です。
 僕のような残念な頭しか持っていない人間にとって、とても助かる勉強会です。

 記念すべき第一回の読書会では、Job Systemについてです。
 ちょうど読書会の開催一週間前ぐらいにテラシュールブログ先生がJob Systemを解説してくださったこともあり、読書会はとてもスムーズに進みました。

tsubakit1.hateblo.jp

 一人でやる場合なら三週間ほどかけて学ぶことも、このような会で学ぶことで三時間ほどで学べるというのは素敵なことですね。
 やはり集中力が違います。
 現に読書会の感想であるこの記事を書くまでに三週間掛かっていますしね。
 一人だとだらけてしまいますよ。

 というわけで学んだJob Systemについてダラダラ書いていこうかなと思っていたのですが、その前にNativeArrayというものについて学んでおかねばなりそうなのです。

 どうやらJob Systemは一筋縄ではいかないようでしてね……。


この記事にはUnity2018.1.0b12を使用しています。
β版を用いた記事のため、正式リリース版とは挙動が異なる可能性があります。

どうしてJob Systemの前にNativeArrayを学ぶのさ

 テラシュールブログ先生に限らず、Job SystemのいろいろなサンプルコードがGitHub上で公開されています。
 そのサンプルコード内でNativeArrayという、今までUnity触ってきて見たことのない構造体が当たり前のように存在しているのですよ。
 どうやらNativeArrayを使うことがJob Systemを使うにあたって必須のようなため、まずNativeArrayから学んでいこうといった感じです。
 僕の頭では複数のことを同時に学ぶことは不可能なものでしてね。

 まあ、NativeArrayは去年のCEDECの講演でもちょいちょい出てきてはいたんですけどね。
 
www.youtube.com

www.slideshare.net

で、NativeArrayとは何なの

 うん、配列だよ(多分)
 指定した値型の配列を作る構造体だよ。
 int[20]、こんなんと一緒だよ。
 当然、まったく一緒ではないようなんだけど。

 テラシュールブログ先生曰く、

C#の安全な配列と異なりアンマネージドコードのバッファをマネージドコードに公開しています。

 アンマネージド……、アウトロー気質な人にはぴったりだね。
 どうやら管理されていないみたいなので、GCで勝手に解放されん代わりに、自分で解放してね、という感じだろうか。
 C++で書いているときと同じ感じかなぁ。

 ちなみに今回出てきたGCという単語は、ゲームキューブの略ではなく、ガベージコレクションの方です。
 ゲームキューブファンの方申し訳ない。

 ガベージコレクションはお母さんみたいなもんだよね。
 お母さんは定期的に僕の部屋を掃除してくれるんだけど、タイミングが悪い時もあるよね。
 ゲームをしているときは掃除機をかけてほしくないんだ。
 ああっ、掃除機がゲーム機に当たってゲームがバグっちゃったじゃない。
 もうっ、お母さんったら!

 こんな感じかな。
 うん、全然説明になっていないな!

 すまん、ググってくれ。
ガベージコレクション - Wikipedia

NativeArrayはJobSystemでどう使われていんの

 JobSystemのマルチスレッドにて、スレッド間のデータをやり取りするバッファに使用しているみたいだね。
 なるほどね。

 でもなんでNativeArrayを使わなくちゃいけなんだろうか……。
 んー、なんでかなぁ。
 この辺、この読書会とは別の交流会の時にUnityの中の人に聞いたんだけどなー。
 人に説明できるまで理解できてなかったようだ……。
 ううー、GCCのときにでも聞こうかなぁ。

じゃあとりあえずNativeArray使おうか

アロケータがあるんだって

 とりあえず使うか。
 えーと、使い方はっと。
 ジェネリックの型に確保する値型を指定して、必要な要素数とアロケータのタイプを指定すればOKっと。
 簡単だね。

TestComponent.cs

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    private void Update()
    {
        var arrayValue = new Unity.Collections.NativeArray<int>( 100, Unity.Collections.Allocator.Temp );
        
        // 特に意味のない処理。    
        for( int i = 0; i < arrayValue.Length; ++i )
        {
            arrayValue[ i ] = i;
        }
    }

} // class TestComponent

f:id:urahimono:20180324135415p:plain

 ……怒られたんですけど。

Internal: JobTempAlloc has allocations that are more than 4 frames old - this is not allowed and likely a leak
A Native Collection has not been disposed, resulting in a memory leak. It was allocated at ...

 リークしてんじゃね? 的なことを言われていますね。
 さっき言った通り、NativeArrayはGCで管理してないので、使わなくなったらちゃんと自分でDispose()を呼んで解放してあげてねとのこと。

 ここがCEDECの講演であった安全って部分みたいだね。
 メモリリークを起こした場合はUnityが駄目だぞっと怒ってくれるので、コードのエラーに気づきやすいってことか。

TestComponent.cs

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    private void Update()
    {
        var arrayValue = new Unity.Collections.NativeArray<int>( 100, Unity.Collections.Allocator.Temp );

        // 特に意味のない処理。
        for( int i = 0; i < arrayValue.Length; ++i )
        {
            arrayValue[ i ] = i;
        }

        // ちゃんと解放してあげよう。
        arrayValue.Dispose();
    }

} // class TestComponent

 ちゃんと自分でメモリを解放することでエラーは出なくなりました。

 ちなみにアロケータタイプってなんだろう。
 メモリを確保する際の割り当て方の種類と書いてあるなぁ。
 こんな種類があるみたい。

Allocator.cs

using UnityEngine.Scripting;

namespace Unity.Collections
{
    //
    // 概要:
    //     Used to specify allocation type for NativeArray.
    [UsedByNativeCode]
    public enum Allocator
    {
        //
        // 概要:
        //     Invalid allocation.
        Invalid = 0,
        //
        // 概要:
        //     No allocation.
        None = 1,
        //
        // 概要:
        //     Temporary allocation.
        Temp = 2,
        //
        // 概要:
        //     Temporary job allocation.
        TempJob = 3,
        //
        // 概要:
        //     Persistent allocation.
        Persistent = 4
    }
}

 InvalidやらNoneは使い道がちょっとわからないけど、おそらく主に使っていくのは、TempTempJobPersistentになるのかな。

 さっきのスクリプトでは、一時的にメモリを確保していればよかったのでAllocator.Tempを使ってみたよ。
 では今度はAllocator.Persistentを使って長期的にメモリが必要なパターンの処理を書いてみよう。

TestComponent.cs

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    private Unity.Collections.NativeArray<int> m_arrayValue;

    private void Start()
    {
        m_arrayValue = new Unity.Collections.NativeArray<int>( 100, Unity.Collections.Allocator.Persistent );
    }

    private void Update()
    {
        // 特に意味のない処理。
        for( int i = 0; i < m_arrayValue.Length; ++i )
        {
            m_arrayValue[ i ] = i;
        }
    }

} // class TestComponent

 今回はアプリが終了するまでメモリを確保しておくつもりだから、Dispose()は呼ばなくていいか、と処理をサボると、終了すると同時に、

f:id:urahimono:20180324135441p:plain

 だよね。
 この場合でもちゃんと解放しようね。

TestComponent.cs

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    private Unity.Collections.NativeArray<int> m_arrayValue;

    private void Start()
    {
        m_arrayValue = new Unity.Collections.NativeArray<int>( 100, Unity.Collections.Allocator.Persistent );
    }

    private void OnDestroy()
    {
        // 終了時にちゃんと解放してあげよう。
        m_arrayValue.Dispose();
    }

    private void Update()
    {
        // 特に意味のない処理。
        for( int i = 0; i < m_arrayValue.Length; ++i )
        {
            m_arrayValue[ i ] = i;
        }
    }

} // class TestComponent

あえてアロケータのタイプを間違える

 では、このアロケータのタイプを誤って使うとどうなるのだろうか。
 実行時に長期的に使うパターンの時に、Allocator.Tempを使ってみよう。

TestComponent.cs

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    private Unity.Collections.NativeArray<int> m_arrayValue;

    private void Start()
    {
        // 長期間確保するにもかかわらず、Tempタイプのアロケータを使っちゃうぞ!
        m_arrayValue = new Unity.Collections.NativeArray<int>( 100, Unity.Collections.Allocator.Temp );
    }

    private void OnDestroy()
    {
        // 終了時にちゃんと解放してあげよう。
        m_arrayValue.Dispose();
    }

    private void Update()
    {
        // 特に意味のない処理。
        for( int i = 0; i < m_arrayValue.Length; ++i )
        {
            m_arrayValue[ i ] = i;
        }
    }

} // class TestComponent

f:id:urahimono:20180324135451p:plain

 なんか警告が出ているぞ。

Internal: JobTempAlloc has allocations that are more than 4 frames old - this is not allowed and likely a leak

 4フレーム以上Tempのメモリを割り当てているけど、リークしてね? だってさ。
 これはAllocator.TempJobを使っても同じ警告文が出ました。
 なるほど、どうやらTempを長期的に使用するメモリの割り当てに使うと警告がでるようだね。
 これは助かる。

 では逆に一時的に使う場合にAllocator.Persistentを使うとどうなるのだろう。

TestComponent.cs

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    private void Update()
    {
        // 一時的にしか使わないにも関わらず、Persistentを指定しちゃうぞ!
        var arrayValue = new Unity.Collections.NativeArray<int>( 100, Unity.Collections.Allocator.Persistent );

        // 特に意味のない処理。
        for( int i = 0; i < arrayValue.Length; ++i )
        {
            arrayValue[ i ] = i;
        }

        // ちゃんと解放してあげよう。
        arrayValue.Dispose();
    }

} // class TestComponent

 特に警告なども出ずに動いてしまうよ。
 い、いいのかな。
 そもそもこの一時的な確保なのに、Allocator.Persistentを使っているのは、Unityドキュメントでもやっているんだよね。

Unity - Scripting API: IJobParallelFor
ApplyVelocityParallelForSample.cs

public void Update()
{
    var position = new NativeArray<Vector3>(500, Allocator.Persistent);

    var velocity = new NativeArray<Vector3>(500, Allocator.Persistent);
    for (var i = 0; i < velocity.Length; i++)
        velocity[i] = new Vector3(0, 10, 0);

    // Initialize the job data
    var job = new VelocityJob()
    {
        deltaTime = Time.deltaTime,
        position = position,
        velocity = velocity
    };

    // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
    // The second parameter is the batch size,
    // essentially the no-overhead innerloop that just invokes Execute(i) in a loop.
    // When there is a lot of work in each iteration then a value of 1 can be sensible.
    // When there is very little work values of 32 or 64 can make sense.
    JobHandle jobHandle = job.Schedule(position.Length, 64);

    // Ensure the job has completed.
    // It is not recommended to Complete a job immediately,
    // since that reduces the chance of having other jobs run in parallel with this one.
    // You optimally want to schedule a job early in a frame and then wait for it later in the frame.
    jobHandle.Complete();

    Debug.Log(job.position[0]);

    // Native arrays must be disposed manually.
    position.Dispose();
    velocity.Dispose();
}

 ただ、テラシュールブログ先生曰く、

Allocator(メモリの割当タイプ)を自分で決める必要がある。
Temp=割当と開放が速いらしい。要1フレームで開放
Persistent=割当と開放が遅いらしい・永続的な割当。要・Destroy等で開放

 らしいので、一時的な確保の場合はAllocator.Tempを使ったほうがよさそうだね。
 ちなみにドキュメントでAllocator.Persistentを使っている件は、Unityの中の人に聞いたところ、
 「ドキュメントは修正が追っついてないんだ。すまねえ。」とのことさね。

usingを使ったほうが楽じゃない

 このDispose()を使って自分で解放する点なんだけど、書き忘れちゃわないかなぁ。
 そりゃエラーは出てくれるみたいだけど、実行時だけだからビルドではエラーにならないんだよね。
 それらusingを使うことで、書き忘れを防止できないかなぁ。
using ステートメント - C# リファレンス | Microsoft Docs

TestComponent.cs

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    private void Update()
    {
        using( var arrayValue = new Unity.Collections.NativeArray<int>( 100, Unity.Collections.Allocator.Temp ) )
        {
            // 特に意味のない処理。
            for( int i = 0; i < arrayValue.Length; ++i )
            {
                // 残念ながら、ここでビルドエラーが出てしまう!
                // なぜなら、usingステートメントを使用している変数は"読み取り専用"だからだ!
                arrayValue[ i ] = i;
            }

            // usingのスコープから外れたときにDispose()が呼ばれるから、明示的に書かなくてもいいはずだ!
            // arrayValue.Dispose();
        }
    }

} // class TestComponent

f:id:urahimono:20180324135505p:plain

 残念ながらこの書き方ではビルドエラー。
 これはNativeArrayが悪いのではなく、usingを使用して作られた変数は読み取り専用になるので、値を書き換えることができないからだね。
 
 ただ、スレッド間の値のやり取りの際に、恐らく値を読み書きするだろうから、ブロック内で読み取り専用はちょっと辛いなぁ。
 それに、長期的にメモリを確保する場合などは、一つの関数内に確保と解放を記述するのも難しそうなので、usingが使える状況はなかなか少なそうだ。

メモリ確保タイミングはいつでもいいのかな

 確保のタイミングについても調べてみよう。
 先ほどまでのスクリプトでは、メモリの確保はStart()Update()で行っていたけど、もっと早いタイミングで確保しても大丈夫なのかな。
 試しにフィールド初期化のタイミングでメモリを確保してみよう。

TestComponent.cs

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    // フィールド初期化のタイミングでメモリを確保しちゃうぞ!
    private Unity.Collections.NativeArray<int> m_arrayValue = new Unity.Collections.NativeArray<int>( 100, Unity.Collections.Allocator.Persistent );

    private void OnDestroy()
    {
        // 終了時にちゃんと解放してあげよう。
        m_arrayValue.Dispose();
    }

    private void Update()
    {
        // 特に意味のない処理。
        for( int i = 0; i < m_arrayValue.Length; ++i )
        {
            m_arrayValue[ i ] = i;
        }
    }

} // class TestComponent

f:id:urahimono:20180324135516p:plain

 エラーが出た。駄目らしい。

A Native Collection has not been disposed, resulting in a memory leak. It was allocated at ...

 アロケータの準備が出来てないのかなぁ。
 とりあえず駄目だってさ。

boolは指定できないからね

 ちなみに、NativeArraybool型は指定できないよ。
 bool型を指定して実行すると、こんな例外が発生してしまう。

TestComponent.cs

using UnityEngine;

public class TestComponent : MonoBehaviour
{
    // NativeArrayをbool型で作っちゃうぞ!
    private Unity.Collections.NativeArray<bool> m_arrayValue;

    private void Start()
    {
        m_arrayValue = new Unity.Collections.NativeArray<bool>( 100, Unity.Collections.Allocator.Persistent );
    }

    private void OnDestroy()
    {
        // 終了時にちゃんと解放してあげよう。
        m_arrayValue.Dispose();
    }

    private void Update()
    {
        // 特に意味のない処理。
        for( int i = 0; i < m_arrayValue.Length; ++i )
        {
            m_arrayValue[ i ] = ( i % 2 == 0 ) ? true : false ;
        }
    }

} // class TestComponent

f:id:urahimono:20180324135526p:plain

ArgumentException: System.Boolean used in NativeArray<System.Boolean> must be blittable

 テラシュールブログ先生で紹介されたC# Job Systemのクックブックでも、bool型で値を使用したいところでもint型の0・1の値を使用している。

github.com

RayBoundsIntersection.cs

struct RayIntersectionJob : IJobParallelFor
{
    [ReadOnly]
    public NativeArray<Bounds> boundsArray;

    // we can't use bools in NativeArray<T>, since the type must be blittable,
    // so i'm representing boolean results as 0 or 1
    [WriteOnly]
    public NativeArray<int> results;

    public Ray ray;

    public void Execute(int i)
    {
        if (boundsArray[i].IntersectRay(ray))
            results[i] = 1;
        else
            results[i] = 0;
    }
}

 NativeArrayで指定するものは、値型でありBlittable型でなくてはいけないようだね。
 bool型は非Blittable型なので、指定できないみたいなんだ。

Blittable 型と非 Blittable 型 | Microsoft Docs

 恐らくマネージコードとアンマネージコード間のマーシャリングが問題になるからかなぁ。

 ちなみになんか物を知っている風に語っているけど、僕自身もさっぱりわかってないからね!
 今回の一件で初めてBlittableやらマーシャリングなどの単語を聞いたレベルさ。
 これはUnityではなくC#側の知識の問題なようだ。
 まだまだ学ぶべきことは多そうだ。

 このブログでもみて勉強しておこうっと。
tech.blog.aerie.jp

 まだJobSystem本編に入っていないにも関わらず、この有様。
 JobSystemは僕にはまだ早かった気がする。