C#を使ったUnity開発で無駄なGCを防ぐためにUpdate内でやるべきじゃないことをまとめました。
- 気を付けるべきことは大きく分けて二つ
- Collectionはの要素数はできるだけ指定しておく
- 一時的なCollectionであってもグローバル変数にして使いまわす
- 空配列を返すような実装は控える
- 匿名メソッドはOKだけどクロージャはダメ
- 固定文字列はグローバル変数、可変文字列はStringBuilderで
- 値型をobject型に変換するとボックス化する
- Profilerでメモリアロケーションを見る方法
- 関連
- 参考
Unity2018.4.0
気を付けるべきことは大きく分けて二つ
Unityでは、使わなくなったメモリをGCを使って解放します。
この処理は結構重く、さらに処理している間動作が止まるので頻繁にGCが走ることは避けなければいけません。
GCが発生する要因は様々ですが、特にUpdateのように頻繁によばれる処理の書き方が悪いと発生しやすくなります。
そこでこの記事では無駄にGCを走らせないように気を付けるべきことを紹介します。
気を付けるべきことは本質的には大きく分けて次の二つとなります。
- Update内で関数内で一時的な参照型をなるべく生成しないこと
- Update内で値型のボックス化を避けること
以下、これらについて具体的なテクニックをまとめていきます。
Collectionはの要素数はできるだけ指定しておく
ListやDictionaryなどのCollectionを要素数を指定せずに生成すると、
要素が最大いくつになるのかわからないのでとりあえずデフォルトの要素数分のメモリを確保します。
そして要素数がこの個数を超えると、新しくCollectionを作ってそちらに中身をコピーします。
これはメモリ的にも処理的にも非効率なので避けるべきです。
Collectionは要素数を指定できるコンストラクタが用意されているため、
要素の最大数がある程度決まっている場合にはこれをコンストラクタで指定しておくと良さそうです。
// 要素数を指定してListを初期化 var example = new System.Collections.Generic.List<int>(100);
一時的なCollectionであってもグローバル変数にして使いまわす
一時的な処理のためにCollectionをローカル変数で定義することはよくあります。
using System.Collections.Generic; using UnityEngine; public class Example : MonoBehaviour { private List<int> _example = new List<int>(); void Update() { var newList = new List<int>(); for (int i = Time.frameCount; i < Time.frameCount + 3; i++) { newList.Add(i); } _example.AddRange(newList); } }
これはもちろんメモリアロケーションを発生させてしまいます。
このようなケースではグローバル変数としてCollectionを定義してそれを使いまわすことでアロケーションを回避できます。
using System.Collections.Generic; using UnityEngine; public class Example : MonoBehaviour { private List<int> _example = new List<int>(); private List<int> _newList = new List<int>(); void Update() { _newList.Clear(); for (int i = Time.frameCount; i < Time.frameCount + 3; i++) { _newList.Add(i); } _example.AddRange(_newList); } }
空配列を返すような実装は控える
次の例のように、バリデーションを通過できなかった場合には空配列を返すような実装をすることがあると思います。
using UnityEngine; public class Example : MonoBehaviour { private int[] _example; void Update() { _example = GetArray(); } private int[] GetArray() { // 何かしらのバリデーションを通過できなかった場合に空配列を返す return new int[0]; } }
実はこのような空配列の生成もメモリアロケーションを発生させます。
これも前節のように空配列のグローバル変数を定義しておくことで回避できます。
using UnityEngine; public class Example : MonoBehaviour { private int[] _example; private int[] _emptyIntArray = new int[0]; void Update() { _example = GetArray(); } private int[] GetArray() { return _emptyIntArray; } }
匿名メソッドはOKだけどクロージャはダメ
using System; using UnityEngine; public class Example : MonoBehaviour { private int _result; void Update() { Func<int, int> exampleMethod = x => { return x + 1; }; _result = exampleMethod(_result); } }
上記の例は特に問題ありません。
ただこの匿名メソッド内でローカル変数を使用した場合、メモリの割り当てが発生するようになります。
このようにローカル変数を使用する匿名メソッドを特にクロージャと呼びます。
using System; using UnityEngine; public class Example : MonoBehaviour { private int _result; void Update() { var source = 1; Func<int, int> exampleMethod = x => { return x + source; }; _result = exampleMethod(_result); } }
基本的に頻繁に呼ばれる処理内ではクロージャは避けるべきです。
ちなみにUnityで使えるのはもう少し先になるとは思いますが、
C#8からはクロージャを防げる静的ローカル関数という仕組みが導入されたようです。
固定文字列はグローバル変数、可変文字列はStringBuilderで
文字列は参照型なので生成すると普通にアロケーションが発生します。
次の例のような書き方でも、参照型のローカル変数をnewしているのと同等の処理になります。
using UnityEngine; public class Example : MonoBehaviour { private string _result; void Update() { var a = "a"; var b = "b"; _result = a + b; } }
また文字列型は一度作ると内容を変更できません。
下記のように書くと一見内容を書き換えているように見えますが、実は新しくインスタンスを生成しているだけです。
var a = "a"; // インスタンスを書き換えているわけではなく新しいインスタンスを作っている a += "b";
このようなことから文字列は、それが固定のものであればできる限りグローバル変数にするべきです。
また、もし動的に変更するようなものにはStringBuilderを使うことで無駄なアロケーションを回避できます。
値型をobject型に変換するとボックス化する
さて値型のローカル変数はスタックに積まれるのでメモリアロケーションは発生しません。
が、値型はobject型に変換できます。
object型は参照型なので、これに変換するとメモリアロケーションが発生してしまいます。
using UnityEngine; public class Example : MonoBehaviour { private object _result; void Update() { var a = 0; _result = a; } }
特にジェネリックを使った設計をしているとついやってしまいがちなので注意が必要です。
ちなみにボックス化については下記の記事で非常にわかりやすく解説されていますので必要に応じて参照してください。
Profilerでメモリアロケーションを見る方法
UnityのProfilerにはメモリアロケーションを見る方法が用意されています。
Update関数で不要なアロケーションがされていないかを見るときにはこれを使うと便利です。