コガネブログ

平日更新を目標に Unity や C#、Visual Studio、ReSharper などのゲーム開発アレコレを書いていきます

【Unity】Unity におけるゲーム開発でガベージコレクションを最適化する(翻訳)

はじめに

この記事は Unity 公式で紹介されている
パフォーマンス改善に関する記事の一部翻訳になります

キャッシング

void OnTriggerEnter( Collider other )
{
    var allRenderers = FindObjectsOfType<Renderer>();
    ExampleFunction( allRenderers );
}

上記の例では、コードが呼び出されるたびに新しい配列が作成されるため、
ヒープの割り当てが行われます

private Renderer[] allRenderers;

void Start()
{
    allRenderers = FindObjectsOfType<Renderer>();
}


void OnTriggerEnter( Collider other )
{
    ExampleFunction( allRenderers );
}

上記のコードでは、作成された配列が Start でキャッシュされるため
ヒープの割り当てが 1 つしか発生しません

キャッシュされた配列は、ゴミを生成することなく、何度も再利用することができます

頻繁に呼び出される関数では割り当てを行わない

Update や LateUpdate は毎フレーム呼び出されるため
これらの関数でガベージを生成するとすぐにゴミが溜まっていきます

可能であれば Start や Awake でオブジェクトへの参照をキャッシュするか、
必要なときにのみ割り当てを行うようにする必要があります

void Update()
{
    ExampleGarbageGeneratingFunction( transform.position.x );
}

例えば上記のコードでは、Update が呼び出されるたびに
割り当てを発生させる関数が呼び出され、頻繁にガベージが作成されます

private float previousTransformPositionX;

void Update()
{
    var transformPositionX = transform.position.x;
    if ( transformPositionX == previousTransformPositionX ) return;
    
    ExampleGarbageGeneratingFunction( transformPositionX );
    previousTransformPositionX = transformPositionX;
}

このように、transform.position.x の値が変更された場合にのみ
関数を呼び出すように変更することで、
必要なときにのみ割り当てが行われるようになります

また、タイマーを使用する方法も有効です

void Update()
{
    ExampleGarbageGeneratingFunction();
}

このコードはゴミを生成する関数が毎フレーム実行されます

private float timeSinceLastCalled;

private float delay = 1f;

void Update()
{
    timeSinceLastCalled += Time.deltaTime;
    if ( timeSinceLastCalled <= delay ) return;
    
    ExampleGarbageGeneratingFunction();
    timeSinceLastCalled = 0f;
}

このコードでは、タイマーを使用して
ゴミを生成する関数が 1 秒に 1 回だけ実行されるようなります

このような変更を、頻繁に実行されるコードに対して行うことで、
生成されるゴミの量を大幅に減らすことができます

コレクションのクリア

新しいコレクションを作成するとヒープに割り当てが行われます
新しいコレクションを作成している場合は、コレクションへの参照をキャッシュして、
new を繰り返し呼び出す代わりに Clear 関数を使用して内容を空にする必要があります

void Update()
{
    var myList = new List<int>();
    PopulateList( myList );
}

この例では、new が使用されるたびに新しいヒープ割り当てが行われます

private List<int> myList = new List<int>();

void Update()
{
    myList.Clear();
    PopulateList( myList );
}

この例では、コレクションが作成された時と、
コレクションのサイズを変更する必要がある時にのみ割り当てが行われます
これにより、ゴミの発生量を削減できます

文字列

文字列の非効率な使用によって不要なゴミを生成するコードの例を紹介します

public Text timerText;
private float timer;

void Update()
{
    timer += Time.deltaTime;
    timerText.text = "TIME:" + timer.ToString();
}

このコードでは、"TIME:"という文字列を float 型のタイマーと組み合わせて
Update 関数でスコア表示用の文字列として作成おり、
これにより不要なゴミが作成されます

public Text timerHeaderText;
public Text timerValueText;
private float timer;

void Start()
{
    timerHeaderText.text = "TIME:";
}

void Update()
{
    timerValueText.text = timer.toString();
}

この例では別の Text コンポーネントに「TIME:」というテキストを設定しています
これにより、Update 関数で文字列を連結する必要が無くなったため、
ゴミの量が大幅に減ります

Unity API の呼び出し

配列を返す Unity 関数やプロパティにアクセスする場合、
新しい配列が作成され、戻り値として渡されることがあるため、
Unity API を呼び出すたびにヒープ割り当てが発生する可能性があります

void ExampleFunction()
{
    for ( int i = 0; i < myMesh.normals.Length; i++ )
    {
        var normal = myMesh.normals[ i ];
    }
}

例えばこのコードは、ループ内で
Mesh.normals にアクセスするたびに新しい配列が作成されます
このような場合、参照を配列にキャッシュするだけで割り当てを減らすことができます

void ExampleFunction()
{
    var meshNormals = myMesh.normals;
    for ( int i = 0; i < meshNormals.Length; i++ )
    {
        ar normal = meshNormals[i];
    }
}

このコードは、ループが実行される前に Mesh.normals を呼び出し、
参照をキャッシュすることで、配列の作成を 1 回だけに抑えています

ヒープ割り当ては、GameObject.tag にアクセスする時にも発生します

private string playerTag = "Player";

void OnTriggerEnter( Collider other )
{
    var isPlayer = other.gameObject.tag == playerTag;
}

このコードでは GameObject.tag の呼び出しによってゴミが生成されます

private string playerTag = "Player";

void OnTriggerEnter( Collider other )
{
    var isPlayer = other.gameObject.CompareTag( playerTag );
}

このように GameObject.CompareTag を使用することで、
ゴミの生成を防ぐことができます

他にも、多くの Unity API の呼び出しは、
ヒープ割り当てを起こさないモノが用意されています
例えば、Input.touches の代わりに Input.GetTouch と Input.touchCount を使用したり、
Physics.SphereCastAll の代わりに Physics.SphereCastNonAlloc を使用することで、
ヒープの割り当てを避けることが可能です

コルーチン

コルーチン内の yield に渡した値によって、
不要なヒープ割り当てが発生することがあります

yield return 0;

例えばこのコードは、0 という int 型の値に対してボックス化が行われてしまう、
不要なヒープ割り当てが発生します

yield return null;

そのため、フレームを待機したい場合はこのコードを使用することがオススメです

また、コルーチンのよくある間違いは、yield で new を使用することです

while ( !isComplete )
{
    yield return new WaitForSeconds( 1f );
}

例えばこのコードはループを繰り返すたびに
WaitForSeconds オブジェクトの作成と破棄が行われてしまいます

var delay = new WaitForSeconds( 1f );

while ( !isComplete )
{
    yield return delay;
}

このように WaitForSeconds オブジェクトをキャッシュして再利用することで
ゴミの発生を防ぐことができます

foreach ループ

Unity のバージョンが 5.5 以前の場合は、foreach ループがゴミを生成します
この問題は Unity 5.5 で修正されました

void ExampleFunction( List<int> listOfInts )
{
    foreach ( int currentInt in listOfInts )
    {
        DoSomething( currentInt );
    }
}

Unity をアップグレードできない場合は、foreach を for に置き換えることで
ゴミの生成を避けることが可能です

void ExampleFunction( List<int> listOfInts )
{
    for ( int i = 0; i < listOfInts.Count; i++ )
    {
        var currentInt = listOfInts[ i ];
        DoSomething( currentInt );
    }
}

データの持ち方を変更する

構造体は値型なのでガベージコレクタの対象外ですが、
構造体に参照型の変数が含まれている場合は、ガベージコレクタの対象となってしまい
ガベージコレクタが実行されるときの負荷が増えてしまいます

public struct ItemData
{
    public string name;
    public int cost;
    public Vector3 position;
}

private ItemData[] itemData;

この例では、構造体に参照型の string が含まれています
そのため、構造体の配列は実行時にガベージコレクタの検査の対象になってしまいます

private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;

このように、データを別々の配列に格納すると、
string の配列のみがガベージコレクタの対象となり、他の配列は無視されます
そのため、ガベージコレクタの負荷を減らすことができます

public class DialogData
{
    private DialogData nextDialog;

    public DialogData GetNextDialog()
    {
        return nextDialog;
    }
}

この例では、ダイアログに対して別のダイアログの参照が格納されています
そのため、ガベージコレクタが実行される時はこの参照も調査の対象になってしまいます

public class DialogData
{
    private int nextDialogID;

    public int GetNextDialogID()
    {
        return nextDialogID;
    }
}

このように、インスタンスを検索するための識別子を保持するように変更すると
オブジェクトの参照が無くなるため、ガベージコレクタの調査の対象外になります

ゲーム中で他のオブジェクトへの参照をたくさん保持している場合
上記のようにインスタンスの識別子を保持するように変更することで
ヒープの複雑さを減らすことができます

関連記事