Unity 開発における C# のパフォーマンス最適化のTips

C#に関するパフォーマンス最適化のTips

はじめに

Unity で C# を扱う際のパフォーマンス最適化につながる Tips を紹介します。
本記事は以下のページを参考に作成しています。

参考 Unityのパフォーマンスに関する推奨事項Microsoft Docs

最適化につながる Tips

GetComponentCamera.main はキャッシュしよう

GetComponent<T>()Camera.main を繰り返し呼ぶのは重いため、初期化時のキャッシュを推奨します。

Camera.main はアクセスするたびに FindGameObjectsWithTag() のタグ検索を行うため、重い処理となります。毎フレーム呼ばれる Update 関数ではキャッシュしたものを参照しましょう。

using System.Collections.Generic;
using UnityEngine;

public class SampleClass : MonoBehaviour
{
    private Camera m_Camera;
    private Transform m_Transform;

    void Start() 
    {
        m_Camera = Camera.main;
        m_Transform = GetComponent<Transform>();
    }

    void Update()
    {
        // Good!
        m_Camera.transform.Rotate(m_Camera.transform.forward, 1f);

        // Bad..
        Camera.main.transform.Rotate(Camera.main.transform.forward, 1f);

        // Good!
        m_Transform.Translate(Vector3.one);

        // Bad..
        GetComponent<Transform>().Translate(Vector3.one);
    }
}

MEMO (2020/12/17 追記)
Unity 2020.2 にて Camera.main のタグ検索の最適化が行われ、およそ 21,000 から 51,000 倍の高速化が確認されました。
詳しくはこちらの Unity Blog「最適化された Camera.main」を御覧ください。

UnityEngine.Object.Find() などの処理は避けよう

UnityEngine.Object.Find()UnityEngine.Object.FindObjectOfType() などの UnityAPI は便利ですが、実行にコストがかかる場合があります。

ゲームオブジェクトの一致するリストをシーン全体で検索することを含むため、参照をキャッシュするなどして使用を回避しましょう。

GameObject.SendMessage()
GameObject.BroadcastMessage()
UnityEngine.Object.Find()
UnityEngine.Object.FindWithTag()
UnityEngine.Object.FindObjectOfType()
UnityEngine.Object.FindObjectsOfType()
UnityEngine.Object.FindGameObjectsWithTag()
UnityEngine.Object.FindGameObjectsWithTag()

SendMessage()BroadcastMessage()は1000倍も遅くなるとか…。

SendMessage() と BroadcastMessage() は、必ず削除する必要があります。これらの関数は直接の関数呼び出しよりも1000倍ほど遅くなる可能性があります。

Microsoft Docs: Unityのパフォーマンスに関する推奨事項

ボックス化に注意しよう

ボックス化は、値型を参照型に変換する処理で発生します。

値型はスタック領域を確保し、参照型はヒープ領域を確保します。ヒープ領域の方がスタック領域に比べて速度が遅く、一時的なコピーが生成されるため、作業にコストがかかります。

以下の場合、値型 (int) から 参照型 (object) へ暗黙的なボックス化が起きています。

int n = 10;
object o = n;  // ボックス化

ボックス化回避策として、厳密な型指定ができる場合は object 型ではなくそちらを使うようにしましょう。

// 暗黙的に int -> object の変換が走る(ボックス化)
object obj = 10;

// int はボックス化が起きない
int obj = 10;

// var は型推論のため、変換は走らない
var obj = 10;

空の Update() は削除しよう

以下のコードは残していても問題なさそうですが、コストがかかっています。

void Update()
{
}

理由は MonoBehaviour が特定のメソッドを持っていた場合に所定のリストに追加されるのですが、例えばスクリプトが Update 関数を持っていると「毎フレーム Update を呼ぶべきスクリプトのリスト」に追加されています。

Unity はゲーム中にこのリストをイテレーションしてメソッドを呼ぶためコストがかかります。詳しい内容は以下の記事に書かれています。

参考 Update()を10000回呼ぶUnity Blog

上記の理由から、Update が空の場合はコードから削除しましょう。

注意
FixedUpdate()LateUpdate() などの繰り返し処理も同様にコストがかかっています。

構造体を値で渡すことは避けよう

struct 構造体は値型であり、直接関数に渡された時にその内容のコピーインスタンスが生成されます。このコピーによりスタックにメモリが追加されます。

小さな構造体の場合は影響が最小限なため許容範囲内です。ただフレームごとに繰り返し呼び出される関数や、大きな構造体を取る関数の場合は参照渡しに変更できないか検討しましょう。

文字列連結は StringBuilder を使おう

String の文字列連結は以下のように + 連結演算子で実現可能です。

Debug.Log("Hello!" + "Unity"); // -> "Hello!Unity"

ただ String は不変であるため変更できず、 + 演算子で文字列を連結する度に新しい文字列が作成されます。これにより多数の文字列を繰り返し変更するとパフォーマンスの低下に繋がります。

一方 StringBuilder は変更可能な文字列クラスであるため、文字列連結の際に新しい文字列が生成されません。

using System;
using System.Reflection;
using System.Text;

public class Example
{    
    public static void Main()
    {
        var sb = new StringBuilder();
        sb.Append("Hello!");
        sb.Append("Unity");
        Debug.Log(sb.ToString()); // -> "Hello!Unity"
    }
}

ループ内で文字列を繰り返し変更する時は、StringBuilder を使うようにしましょう。

参考 String 型と StringBuilder 型Microsoft Docs