LoginSignup
18
10

More than 3 years have passed since last update.

UnityでGC Allocを発生させないC#コーディング

Posted at

Unityでコードを書いたときにGC Allocが発生するパターンを調べた。GC Allocが少なくなるパターンをgoodとしたが、コードがわかりづらくなる場合もあるので必ずしも書き換えをする必要はない。

Unity 2018.4 で動作確認

文字と数値の連結

bad
int i = 123;
string s = "num_" + i;
good
int i = 123;
string s = "num_" + i.ToString();

string.Concatにobjectとして渡されてボックス化されてしまうので文字列にする。

4つまでの文字列連結

bad
string[] num = { "0", "1", "2", "3", "4", "5" };
// 96byte
s = num[0];
s += num[1];
s += num[2];
s += num[3];
good
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)が呼ばれて一度に連結できる。

ループ中の文字列連結

good
var sb = new System.Text.StringBuilder();
for (int i = 0; i < num.Length; i++)
{
    sb.Append(num[i]);
}
string s = sb.ToString();

ループでたくさん文字列連結することがある場合は、一回ごとに文字列が生成されないようにStringBuilderを使う。

文字列の配列の連結

good
string[] num = { "0", "1", "2", "3", "4", "5" };
// 38byte
string s = string.Join("", num);

文字列の配列を連結する場合はJoinを使う。

string.Format

bad
string[] num = new string[10];
for (int i = 0; i < num.Length; i++)
{
    // 84byte
    num[i] = string.Format("num_{0}", i);
}
good
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のキャスト

bad
// 40byte
EnumType e = (EnumType)System.Enum.ToObject(typeof(EnumType), i);
// 40byte
i = System.Convert.ToInt32(e);
good
// 0byte
EnumType e = (EnumType)i;
// 0byte
i = (int)e;

EnumをDictionaryのKeyにしない(.NET 3.5)

bad
var d = new Dictionary<EnumType, int>();
good
var d = new Dictionary<int, int>();

.NET 4ならボックス化しないのでEnumをKeyにしてアクセスしてもGC Allocは発生しないが、低速なのでintをKeyにしたほうがいい。

コルーチンの戻り値

bad
// 20byte
yield return 0;
good
// 0byte
yield return null;

0を戻り値にした場合、ボックス化してGC Allocが発生する。

コルーチンでのforeach

bad
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());
good
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のサイズ指定をする

bad
List<int> list = new List<int>();
// 8.3Kbyte
for (int i = 0; i < 1000; i++)
{
    list.Add(i);
}
good
List<int> list = new List<int>(1000);
// 4.0Kbyte
for (int i = 0; i < 1000; i++)
{
    list.Add(i);
}

ListやDictionaryはあらかじめ追加するサイズが分かっている場合、生成するときにCapacityを指定する。

配列のソート

bad
int[] array = new int[100];
// 10.9Kbyte
for (int i = 0; i < 100; i++)
{
    Array.Sort(array);
}
good
int[] array = new int[100];
// 112byte
for (int i = 0; i < 100; i++)
{
    Array.Sort(array, (x, y) => x - y);
}

たとえintの配列であってもラムダ式を指定しないと不要なキャストが発生してしまう。(.NET Coreだと大丈夫そう)

Delegate

bad
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);
}
good
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);
}

何度も呼び出す関数はキャッシュする。

ラムダ式のスコープ

bad
List<int> RemoveList(List<int> list, int v)
{
    if (list != null && list.Count > 0)
    {
        return list.FindAll(x => x != v);
    }
    return null;
}
good
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のループ

bad
IList list = new string[10];
foreach (var e in list) {}
good
IList list = new string[10];
for (int i = 0; i < list.Count; i++) {}

インターフェース経由でforeachを使うとボックス化が発生するのでforを使う。

メンバーの順番

bad
// 80byte
class C
{
    byte a;
    long b;
    byte c;
    long d;
    byte e;
    long f;
    byte g;
    long h;
}
good
// 56byte
class C
{
    long b;
    long d;
    long f;
    long h;
    byte a;
    byte c;
    byte e;
    byte g;
}

Unityではclassでも自動で適切なメモリレイアウトにならないようなので無駄なパディングが入らないようにメンバー定義の順番を考慮する。

エンコーディング

bad
string s = "hoge";
int n = System.Text.Encoding.GetEncoding("UTF-8").GetByteCount(s);
good
string s = "hoge";
int n = System.Text.Encoding.UTF8.GetByteCount(s);

GetEncodingを呼び出すと毎回クラスが生成されるのでキャッシュされるUTF8を使う。

UnityのAPI

Object.name

bad
for (int i = 0; i < 4; i++)
{
    Debug.Log(go.name);
}

name プロパティにアクセスするたびにGC Allocが発生するのでキャッシュする。

Application.***Path

  • Application.persistentDataPath
  • Application.temporaryCachePath

などもアクセスするたびに文字列が生成されてしまうのでキャッシュする。

Renderer.materials

bad
for (int i = 0; i < renderer.materials.length; i++)
{
}
good
foreach (var m in renderer.materials)
{
}

Renderer.materials はプロパティで配列を生成して返しているのでループで毎回アクセスしないようにする。

Input.touches

bad
// 80byte
foreach (var touch in Input.touches)
{
}
good
// 0byte
for (int i = 0; i < Input.touchCount; i++)
{
    var touch = Input.GetTouch(i);
}

こちらも配列を生成して返すので Input.GetTouch を使う。

Animator.GetParameter

bad
for (int i = 0; i < animator.parameterCount; i++)
{
    // animators.parameters[i].nameとほぼ同じ
    Debug.Log(animator.GetParameter(i).name);
}
good
foreach (var paramter in animator.parameters)
{
    Debug.Log(paremter.name);
}

Animator.parameters は配列を返すAPIなのでキャッシュする。
Animator.GetParameterAnimator.parameters を内部で呼び出しているだけなので使わない。

結果を配列で返すAPI

good
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)

何度も呼び出す場合は、引数に配列やリストを渡して結果を受け取る関数を使う。

18
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
10