クラゲ日記

UnityのTipsなど技術関連がメイン。ゆるゆる更新します。

UI Panel 制御のための Unity Transform のキャッシュ方式比較

結果のメモのようなものです。 百万回くらいやられていそうなTransformのキャッシングの比較をしました。

テストのモチベーション

構造化を考えてUIを作っていると、パネルという概念があることに気付きます。 例えば、RPGのパーティのステータスを表示するということを考えると、例えばUnity+NGUIならこんな感じになるでしょう。

f:id:kurage-create:20150615013740p:plain

これをコードから制御します。 classを実装してUIを制御するならパネル1種類に1classくらいが適当でしょう。すると、例えばこんな感じになります。

class CharacterPanelControl : MonoBehaviour{
...
  public void UpdateHP(int hp){
    Transform hpTrans = transform.FindChild("HPLabel");
    hpTrans.GetComponent<UILabel>().text = hp.ToString();
  }
...
}

transform.FindChild(); がちょっと遅そうですが、大抵の場合はこれで十分です。初期化時など、パネルの情報を丸ごと書き換える場合は transform.FindChild() がたくさん呼ばれますが、それでも1秒に一回程度ならそこまで問題にはなりません。 ただ、毎フレームに近いレベルになるとちょっと不安です。たまたまそれに近いことが起こりそうだったので調査してみた次第です。

テストの問題設定

  • classから実行中に変化しない相対パスを持つ100個の子Transformに繰り返しランダムアクセスを行う
  • 各キャッシュ方式でのアクセス速度の比較。

です。方式によってはキャッシュを実装するコストがバカにならないので、必ずしも速ければいいというものではありません。

テストコードの抜粋

...
public class PerformanceTest : MonoBehaviour {

    Dictionary<string, Transform> mCachedTransform;
    List<Transform> mCachedList;
    private string[] mPathes;
    private Transform[] mTransforms;
    private Transform mSelf;
    private enum eCache{
        eHoge,
        eFuga, 
        eMoge,
    };
    const int NUM = 100;
...
    private void StartTest(){
        const int TEST_NUM = 100000;
        int[] indexes = new int[TEST_NUM];
        for( int i = 0; i < TEST_NUM; ++i ){
            indexes[i] = Random.Range(0, mPathes.Length);
        }
        Transform temp = null;
        float time, take;

        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mSelf.Find(mPathes[indexes[i]]);
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("mSelf.Find(): " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mSelf.FindChild(mPathes[indexes[i]]);
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("mSelf.FindChild(): " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = transform.Find(mPathes[indexes[i]]);
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("transform.Find(): " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = transform.FindChild(mPathes[indexes[i]]);
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("transform.FindChild(): " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mCachedTransform[mPathes[indexes[i]]];
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("string Dictionary cache: " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mCachedList[indexes[i]];
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("List cache: " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mTransforms[1];
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("Array cache: " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mTransforms[(int)eCache.eFuga];
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("Array cache with enum cast: " + take.ToString() );
        }
...
}

テストコードの全文は最後に。全コピペでどこかにAddComponentして実行すればAキーで実行出来ると思います。

出力結果

mSelf.Find(): 0.02063704
mSelf.FindChild(): 0.01855803
transform.Find(): 0.02024984
transform.FindChild(): 0.0211978
string Dictionary cache: 0.01583314
List cache: 0.001600742
Array cache: 0.0006182194
Array cache with enum cast: 0.0006425381

結論

  • 平均して1フレームに1回レベルでアクセスがあるなら、Array cache with enum cast を使用
  • それ以上なら今まで通り transform.FindChild() を使用

enum cast を試したのは、流石にintリテラルを前提には出来ないからです。普通のintアクセスと比べると一応遅くはなってますが問題ないでしょう。 (Array cacheはindexes[]へのintランダムアクセス分他より速くなっています。コードを変えれば分かりますが、List cacheの約2倍速いという点では同じでした) FindChildも個人的には予想通りというか、全然いいじゃんというレベルでした。

キャッシュ用基底クラスを作ってみる

では実際に、キャッシュを使う場合の基底用オレオレBehaviourクラスを作ると、こんな感じかなと思います。

public abstract class TransCacheBehaviour : MonoBehaviour {
    private Transform[] mCachedTrans;

    protected void InitCache( string[] cachePath ){
        mCachedTrans = new Transform[(int)cachePath.Length];
        for( int i =0; i < cachePath.Length; ++i ){
            mCachedTrans[i] = transform.FindChild(cachePath[i]);
        }
    }
    protected Transform CachedTrans(int index){
        return mCachedTrans[index];
    }
}

使い方としてはこうなります。

public class CharacterPanelControl : TransCacheBehaviour {
    private enum eCacheChilds{
        HPLabel,
        UnitNameLabel,
        JobNameLabel,
    }
    private static string[] CACHE_PATH = {
        "HPLabel",
        "UnitNameLabel",
        "JobNameLabel"
    };

    void Awake(){
        InitCache( CACHE_PATH );
    }
    public void UpdateHP(int hp ){
        CachedTrans( (int)eCacheChilds.HPLabel ).GetComponent<UILabel>().text = hp.ToString();
    }
...
}

eCacheChilds.HPLabel.ToString()の結果は"HPLabel"になるのでCACHE_PATHはコードの重複になりますが、enumのToStringは非常に遅いので注意が必要です。

そのほか

テストの目的は以上で達成出来たかなと思いますが、得られたその他の知見をいくつか挙げてみます。

Find? FindChild??

速度同じやん……。 ぶっちゃけどう違うのか分かってなかったんですが、今ぐぐったらあっさりと…Finding Children question - Unity Answers 要はFindChildがFindのラッパですね。理屈上はFindの方が僅かに速いんでしょう。

this.transform は遅い?

風の噂で聞いたのでついでに。FindChild()のオーバーヘッドに比べれば気にならないくらいですね。 ただ、Unity - this.transformを計測してみた - Qiitaによるとやはり速いには速いようなので、オレオレMonoBehaviourを作るならついでにキャッシュしておいてもいいかもしれません。

Dictionary<string, Transform>はいいやつだったよ…

これが使える速さならキャッシュの下準備がラクになると思ったのですが、キャッシュする価値はないレベルですね……。 FindChildがオマケのついたルックアップみたいなもんだろうとたかをくくっていたら、思ったより速いんですね。

List のランダムアクセス

Listのランダムアクセス、Arrayと同じだと思っていたら、倍も遅いんですね。何かオマケの処理でも入ってるんでしょうか。

以下、テストコードの全文です。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class PerformanceTest : MonoBehaviour {

    Dictionary<string, Transform> mCachedTransform;
    List<Transform> mCachedList;
    private string[] mPathes;
    private Transform[] mTransforms;
    private Transform mSelf;
    private enum eCache{
        eHoge,
        eFuga, 
        eMoge,
    };

    //ランダム文字列
    private static string chars = "0123456789abcdefghijklmnopqrstuvwxyz";

    const int NUM = 100;

    private string GetRandomStr(int length ){
        string result = "";
        //new string('x', length);
        int cLength =  chars.Length;
        for( int i = 0; i < length; ++i ){
            result += chars[Random.Range(0, cLength)];
        }
        return result;
    }

    // Use this for initialization
    void Start () {
        mSelf = transform;
        mPathes = new string[NUM];
        mTransforms = new Transform[NUM];
        mCachedTransform = new Dictionary<string, Transform>();
        mCachedList = new List<Transform>();
        for( int i  =0; i < NUM; ++i ){
            string path = GetRandomStr(10);
            if( mCachedTransform.ContainsKey( path ) ){
                i--;
                continue;
            }
            var go = new GameObject(path);
            mPathes[i] = path;
            mTransforms[i] = go.transform;
            mCachedList.Add(go.transform);
            mCachedTransform.Add( path, go.transform );
        }
    }

    // Update is called once per frame
    void Update () {
        if( Input.GetKeyDown(KeyCode.A)){
            StartTest();
        }
    }

    private void StartTest(){
        const int TEST_NUM = 100000;
        int[] indexes = new int[TEST_NUM];
        for( int i = 0; i < TEST_NUM; ++i ){
            indexes[i] = Random.Range(0, mPathes.Length);
        }
        Transform temp = null;
        float time, take;

        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mSelf.Find(mPathes[indexes[i]]);
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("mSelf.Find(): " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mSelf.FindChild(mPathes[indexes[i]]);
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("mSelf.FindChild(): " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = transform.Find(mPathes[indexes[i]]);
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("transform.Find(): " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = transform.FindChild(mPathes[indexes[i]]);
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("transform.FindChild(): " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mCachedTransform[mPathes[indexes[i]]];
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("string Dictionary cache: " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mCachedList[indexes[i]];
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("List cache: " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mTransforms[1];
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("Array cache: " + take.ToString() );
        
        time = Time.realtimeSinceStartup;
        for( int i = 0; i < TEST_NUM; ++i ){
            temp = mTransforms[(int)eCache.eFuga];
        }
        take = Time.realtimeSinceStartup - time;
        Debug.Log("Array cache with enum cast: " + take.ToString() );
        
    }

    
}