京都で行われるUnity技術者の集いである京ゆににて、アセット読書会なる会が開かれました。
各自好き勝手Unityを触っているもくもく会とは違い、Unityの特定の技術を一緒に勉強していこうよ、という会です。
僕のような残念な頭しか持っていない人間にとって、とても助かる勉強会です。
記念すべき第一回の読書会では、Job Systemについてです。
ちょうど読書会の開催一週間前ぐらいにテラシュールブログ先生がJob Systemを解説してくださったこともあり、読書会はとてもスムーズに進みました。
一人でやる場合なら三週間ほどかけて学ぶことも、このような会で学ぶことで三時間ほどで学べるというのは素敵なことですね。
やはり集中力が違います。
現に読書会の感想であるこの記事を書くまでに三週間掛かっていますしね。
一人だとだらけてしまいますよ。
というわけで学んだJob Systemについてダラダラ書いていこうかなと思っていたのですが、その前にNativeArray
というものについて学んでおかねばなりそうなのです。
どうやらJob Systemは一筋縄ではいかないようでしてね……。
この記事にはUnity2018.1.0b12を使用しています。
β版を用いた記事のため、正式リリース版とは挙動が異なる可能性があります。
- どうしてJob Systemの前にNativeArrayを学ぶのさ
- で、NativeArrayとは何なの
- NativeArrayはJobSystemでどう使われていんの
- じゃあとりあえずNativeArray使おうか
どうしてJob Systemの前にNativeArrayを学ぶのさ
テラシュールブログ先生に限らず、Job SystemのいろいろなサンプルコードがGitHub上で公開されています。
そのサンプルコード内でNativeArray
という、今までUnity触ってきて見たことのない構造体が当たり前のように存在しているのですよ。
どうやらNativeArray
を使うことがJob Systemを使うにあたって必須のようなため、まずNativeArray
から学んでいこうといった感じです。
僕の頭では複数のことを同時に学ぶことは不可能なものでしてね。
まあ、NativeArray
は去年のCEDECの講演でもちょいちょい出てきてはいたんですけどね。
www.youtube.com
で、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
……怒られたんですけど。
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
は使い道がちょっとわからないけど、おそらく主に使っていくのは、Temp
、TempJob
、Persistent
になるのかな。
さっきのスクリプトでは、一時的にメモリを確保していればよかったので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()
は呼ばなくていいか、と処理をサボると、終了すると同時に、
だよね。
この場合でもちゃんと解放しようね。
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
なんか警告が出ているぞ。
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
残念ながらこの書き方ではビルドエラー。
これは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
エラーが出た。駄目らしい。
A Native Collection has not been disposed, resulting in a memory leak. It was allocated at ...
アロケータの準備が出来てないのかなぁ。
とりあえず駄目だってさ。
boolは指定できないからね
ちなみに、NativeArray
にbool
型は指定できないよ。
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
ArgumentException: System.Boolean used in NativeArray<System.Boolean> must be blittable
テラシュールブログ先生で紹介されたC# Job Systemのクックブックでも、bool
型で値を使用したいところでもint
型の0・1の値を使用している。
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は僕にはまだ早かった気がする。