C#
Unity3D
Unity
uGUI

RectTransformをなるはやで取得したい

Unity標準のUIといえば俗にいう「uGUI」です。

これらuGUIコンポーネントは通常のTransformではなくて、RectTransformを使う必要があります。

ところで。

みなさん、RectTransformをスクリプトで取得する時にはGetComponent<RectTransform>()ってやってるんじゃないかと 思います。
でも知ってました?RectTransformってTransformのサブクラスなんですよ。

なので、

var rectTransform1 = GetComponent<RectTransform>();
var rectTransform2 = transform as RectTransform;

実はこの2つとも正しくRectTransformが取得できます。

すると気になるのが速度です。 『遅い遅い、何やってんだ、しっかりしろ、どうなってるんだ。』 と評判のGetComponentが出てくるからには速度が気になります。

というわけで調べてみました。

GetComponent vs Cast vs Cache

検証コード
using UnityEngine;
using UnityEngine.Profiling;

public class SpeedCheck : MonoBehaviour
{
    private readonly int max = 10000;

    private CustomSampler samplerMemberCache;
    private CustomSampler samplerGetComponent;
    private CustomSampler samplerAsCast;

    private RectTransform cachedRectTransform;

    void Start()
    {
        //サンプラー用意
        samplerGetComponent = CustomSampler.Create("GetComponent");
        samplerAsCast = CustomSampler.Create("AsCast");
        samplerMemberCache = CustomSampler.Create("MemberCache");

        //cache
        cachedRectTransform = GetComponent<RectTransform>();
    }

    void Update()
    {
        //GetComponent
        samplerGetComponent.Begin();
        for(var i = 0;i < max;++i)GetComponent<RectTransform>().Rotate(0,0,Time.deltaTime);
        samplerGetComponent.End();

        //as演算子
        samplerAsCast.Begin();
        for (var i = 0; i < max; ++i) (transform as RectTransform).Rotate(0, 0, Time.deltaTime);
        samplerAsCast.End();

        //メンバ変数キャッシュ
        samplerMemberCache.Begin();
        for (var i = 0; i < max; ++i) cachedRectTransform.Rotate(0, 0, Time.deltaTime);
        samplerMemberCache.End();
    }
}

結果
image.png

GetComponent:2.73ms
AsCast:2.53ms
MemberCache:2.28ms

うーん。 予想を裏切らない、

「GetComponet使うなら、asでキャストしたほうが良い。 でもやっぱりキャッシュが一番速いよね」

という思った通りの結果となりました。

+ ExtendMethodAsCast vs PropertyCache

(まぁ、メンバ変数へのキャッシュが最速なのはさておき) As演算子でキャストするのは良いんですが、そのまま使おうとすると括弧を一つ挟まなくてはいけないので若干面倒です。

なので、こういう拡張メソッドを用意したらどうでしょう

as演算子拡張メソッド版
public static class RectTransformExtension
{
    public static RectTransform ToRectTransform(this Transform t)
    {
        return t as RectTransform;
    }
}

それと、メンバ変数へのキャッシュはうっかりStart(Awake)でキャッシュを入れ忘れたらまずいですよね。
なので、こういうことをよくやります。

初回アクセス時にキャッシュを自動でするマン(プロパティキャッシュ)
    private RectTransform _cachedRectTransform;
    public RectTransform RectTransform => 
        _cachedRectTransform != null ? _cachedRectTransform : (_cachedRectTransform = transform as RectTransform);

さてこれらは速いんでしょうか。(「プロパティキャッシュ」と本当に呼ぶかどうかは知りません。勝手につけました)

検証コードに以下を追加して、再検証してみます。

        //as演算子 拡張メソッド版
        samplerMethodExtendAsCast.Begin();
        for (var i = 0; i < max; ++i) transform.ToRectTransform().Rotate(0, 0, Time.deltaTime);
        samplerMethodExtendAsCast.End();

        //プロパティキャッシュ
        samplerPropertyCache.Begin();
        for (var i = 0; i < max; ++i) RectTransform.Rotate(0, 0, Time.deltaTime);
        samplerPropertyCache.End();

結果
image.png

GetComponent:2.95ms
AsCast:2.61ms
MemberCache:2.29ms
拡張メソッドAsCast:2.66ms
プロパティキャッシュ:2.68ms

拡張メソッドの方は、メソッド呼び出しで若干オーバーヘッドが出るので遅くなることは想定してましたが、微々たるものですね。悪くないです。

ところが、予想に反してプロパティキャッシュがやたら遅いです。Castに負けるってありえなくないですか?なんででしょう?????

(UnityEngine.Object)のnullチェック! またお前か!

普段からunityをお使いの紳士・淑女はご存知の方も多いと思います。 UnityEngine.Objectを継承しているクラスにおいて、nullチェックはnullチェックではなく、nullチェック+生存確認です。

なので、

    public RectTransform RectTransform => 
        _cachedRectTransform != null ? _cachedRectTransform : (_cachedRectTransform = transform as 

        _cachedRectTransform != null

        _cachedRectTransform.IsAlive() //こんなメソッドはありません。あくまでも概念です

をやっているようなもので、遅い原因はコイツです。

ここでは、別に生存を取りたいわけではなくて、キャッシュしてあるかどうかを判断するためのnullチェックなので、いってしまえばbool的な使い方です。
試しにちょっとソースコードが太りますが、プロパティキャッシュを以下のように変更してみます。

    private RectTransform _cachedRectTransform;
    //    public RectTransform RectTransform => _cachedRectTransform != null ? _cachedRectTransform : (_cachedRectTransform = transform as RectTransform); //これは辞めて
    private bool isCached;
    public RectTransform RectTransform
    {
        get
        {
            if (!isCached)
            {
                _cachedRectTransform = transform as RectTransform;
                isCached = true;
            }
            return _cachedRectTransform;
        }
    }

結果
image.png

GetComponent:3.16ms
AsCast:2.53ms
MemberCache:2.24ms
拡張メソッドAsCast:2.73ms
プロパティキャッシュ:2.37ms

GetComponent > 拡張メソッドAsCast > AsCast > プロパティキャッシュ > MemberCache

概ね想像通りの結果となりました。

なお。今回はわかりやすさ優先でわざわざboolの変数(isCached)を用意しましたが、
??(null合体演算子) を使って

    private RectTransform _cachedRectTransform;
    public RectTransform RectTransform => _cachedRectTransform ?? (_cachedRectTransform = transform as RectTransform);

こう書いた方が効率が良いです。(タイピング量的にも、処理的にも)

一応、計測結果もペタリ
image.png

まとめ

まぁ、実の所GetComponentもそんな禿げ上がるほど遅くはないので、よほどピーキーなチューニングが必要な環境でもない限り使ったっていいんじゃないの?という気もします。(マサカリ案件)
次点でプロパティ使ったキャッシュかなーと、というのもただでさえ汚いStartやAwakeをあまり汚したくないのと、プロパティキャッシュ方式ならスニペットとかで簡単にコード挿入できるかなーとか。(あくまでも個人的な意見です。 ここら辺のキャッシュを加味した MonoBehaviour継承のクラスを使うのはまぁ好き好きで)(マサカリ案件その2)

何にせよ、Unityには素敵なProfilerが最初から付いてるので、(心と時間に)余裕があれば計測しましょう。
知らないで使うのと、知って使うのとでは色々と見える世界が変わってくると思います。
それでは。