Unityでコードを書いたときにGC Allocが発生するパターンを調べた。GC Allocが少なくなるパターンをgoodとしたが、コードがわかりづらくなる場合もあるので必ずしも書き換えをする必要はない。
Unity 2018.4 で動作確認
文字と数値の連結
int i = 123;
string s = "num_" + i;
int i = 123;
string s = "num_" + i.ToString();
string.Concatにobjectとして渡されてボックス化されてしまうので文字列にする。
4つまでの文字列連結
string[] num = { "0", "1", "2", "3", "4", "5" };
// 96byte
s = num[0];
s += num[1];
s += num[2];
s += num[3];
string[] num = { "0", "1", "2", "3", "4", "5" };
// 34byte
string s = num[0] + num[1] + num[2] + num[3];
連結するごとに新しい文字列が生成されるが、4つまでの連結ならConcat(string, string, string, string)が呼ばれて一度に連結できる。
ループ中の文字列連結
var sb = new System.Text.StringBuilder();
for (int i = 0; i < num.Length; i++)
{
sb.Append(num[i]);
}
string s = sb.ToString();
ループでたくさん文字列連結することがある場合は、一回ごとに文字列が生成されないようにStringBuilderを使う。
文字列の配列の連結
string[] num = { "0", "1", "2", "3", "4", "5" };
// 38byte
string s = string.Join("", num);
文字列の配列を連結する場合はJoinを使う。
string.Format
string[] num = new string[10];
for (int i = 0; i < num.Length; i++)
{
// 84byte
num[i] = string.Format("num_{0}", i);
}
string[] num = new string[10];
for (int i = 0; i < num.Length; i++)
{
// 64byte
num[i] = $"num_{i.ToString()}";
}
string.Formatよりも文字列補間($"")を使う。文字列補間のほうが見やすく、引数が文字列だけの場合は単純な文字列連結になりstring.Formatが呼び出されない。
StringBuilderへの数値の追加
var sb = new System.Text.StringBuilder();
int n = 100;
sb.Append(n);
.NET Frameworkでは整数はToStringされて追加される。ToStringしたくない場合、1文字ずつ追加する必要がある。
https://gist.github.com/sapphire-al2o3/ba7d6a80836a2e5ee117abb4c3d75132
Enumのキャスト
// 40byte
EnumType e = (EnumType)System.Enum.ToObject(typeof(EnumType), i);
// 40byte
i = System.Convert.ToInt32(e);
// 0byte
EnumType e = (EnumType)i;
// 0byte
i = (int)e;
EnumをDictionaryのKeyにしない(.NET 3.5)
var d = new Dictionary<EnumType, int>();
var d = new Dictionary<int, int>();
.NET 4ならボックス化しないのでEnumをKeyにしてアクセスしてもGC Allocは発生しないが、低速なのでintをKeyにしたほうがいい。
コルーチンの戻り値
// 20byte
yield return 0;
// 0byte
yield return null;
0を戻り値にした場合、ボックス化してGC Allocが発生する。
コルーチンでのforeach
IEnumrator Sum(int[] array)
{
int s = 0;
foreach (var a in array)
{
s += a;
yield return null;
}
}
int[] array = { 1, 2, 3, 4, 5 };
StartCoroutine(Sum());
IEnumrator Sum(int[] array)
{
int s = 0;
for (int i = 0; i < array.Length; i++)
{
s += a[i];
yield return null;
}
}
int[] array = { 1, 2, 3, 4, 5 };
StartCoroutine(Sum());
foreachよりもforのほうが一時変数が少ないのでコルーチンのサイズが小さくなる。
Listのサイズ指定をする
List<int> list = new List<int>();
// 8.3Kbyte
for (int i = 0; i < 1000; i++)
{
list.Add(i);
}
List<int> list = new List<int>(1000);
// 4.0Kbyte
for (int i = 0; i < 1000; i++)
{
list.Add(i);
}
ListやDictionaryはあらかじめ追加するサイズが分かっている場合、生成するときにCapacityを指定する。
配列のソート
int[] array = new int[100];
// 10.9Kbyte
for (int i = 0; i < 100; i++)
{
Array.Sort(array);
}
int[] array = new int[100];
// 112byte
for (int i = 0; i < 100; i++)
{
Array.Sort(array, (x, y) => x - y);
}
たとえintの配列であってもラムダ式を指定しないと不要なキャストが発生してしまう。(.NET Coreだと大丈夫そう)
Delegate
void Log(int value) { Debug.Log(value); }
void Squared(int value, Action<void> callback)
{
int v = value * 2;
callback(v);
}
int[] num = { 0, 1, 2, 3, 4 };
foreach (var n in num)
{
Squared(n, Log);
}
void Log(int value) { Debug.Log(value); }
void Squared(int value, Action<void> callback)
{
int v = value * 2;
callback(v);
}
int[] num = { 0, 1, 2, 3, 4 };
var log = Log;
foreach (var n in num)
{
Squared(n, log);
}
何度も呼び出す関数はキャッシュする。
ラムダ式のスコープ
List<int> RemoveList(List<int> list, int v)
{
if (list != null && list.Count > 0)
{
return list.FindAll(x => x != v);
}
return null;
}
List<int> RemoveList(List<int> list, int v)
{
if (list != null && list.Count > 0)
{
int t = v;
return list.FindAll(x => x != t);
}
return null;
}
listがnullの場合は何もしないがbadのほうは関数を呼び出しただけでGC Allocが発生する。
関数に入ったときに引数をキャプチャしたラムダ式が生成されてしまうので引数を内側のスコープにキャッシュしてキャプチャするとスコープに入らない場合はラムダ式が生成されない。
IListのループ
IList list = new string[10];
foreach (var e in list) {}
IList list = new string[10];
for (int i = 0; i < list.Count; i++) {}
インターフェース経由でforeachを使うとボックス化が発生するのでforを使う。
メンバーの順番
// 80byte
class C
{
byte a;
long b;
byte c;
long d;
byte e;
long f;
byte g;
long h;
}
// 56byte
class C
{
long b;
long d;
long f;
long h;
byte a;
byte c;
byte e;
byte g;
}
Unityではclassでも自動で適切なメモリレイアウトにならないようなので無駄なパディングが入らないようにメンバー定義の順番を考慮する。
エンコーディング
string s = "hoge";
int n = System.Text.Encoding.GetEncoding("UTF-8").GetByteCount(s);
string s = "hoge";
int n = System.Text.Encoding.UTF8.GetByteCount(s);
GetEncodingを呼び出すと毎回クラスが生成されるのでキャッシュされるUTF8を使う。
UnityのAPI
Object.name
for (int i = 0; i < 4; i++)
{
Debug.Log(go.name);
}
name
プロパティにアクセスするたびにGC Allocが発生するのでキャッシュする。
Application.***Path
- Application.persistentDataPath
- Application.temporaryCachePath
などもアクセスするたびに文字列が生成されてしまうのでキャッシュする。
Renderer.materials
for (int i = 0; i < renderer.materials.length; i++)
{
}
foreach (var m in renderer.materials)
{
}
Renderer.materials
はプロパティで配列を生成して返しているのでループで毎回アクセスしないようにする。
Input.touches
// 80byte
foreach (var touch in Input.touches)
{
}
// 0byte
for (int i = 0; i < Input.touchCount; i++)
{
var touch = Input.GetTouch(i);
}
こちらも配列を生成して返すので Input.GetTouch
を使う。
Animator.GetParameter
for (int i = 0; i < animator.parameterCount; i++)
{
// animators.parameters[i].nameとほぼ同じ
Debug.Log(animator.GetParameter(i).name);
}
foreach (var paramter in animator.parameters)
{
Debug.Log(paremter.name);
}
Animator.parameters
は配列を返すAPIなのでキャッシュする。
Animator.GetParameter
は Animator.parameters
を内部で呼び出しているだけなので使わない。
結果を配列で返すAPI
void GetComponentsInChildren (List<T> results)
void GetComponentsInChildren (List<T> results)
void GetComponentsInParent (bool includeInactive, List<T> results)
int Physics.RaycastNonAlloc (... , RaycastHit[] results, ...)
Animator.GetCurrentAnimatorClipInfo (int layerIndex, List<AnimatorClipInfo> clips)
何度も呼び出す場合は、引数に配列やリストを渡して結果を受け取る関数を使う。