C#
Unity
GetComponent

VS GetComponentおじさん

前置き

  • 使用したUnityは2018.1.0b13です。
  • ネタのようなネタじゃないような話です。

君はGetComponentおじさんを知っているか

「僕Unityできますよ」Tシャツのコードレビュー

でも触れられていましたが、
「GetComponentは遅いからUpdate内で使うなんてトンデモナイ!」
というのが定説です。いや、定説というか事実なんですが。

それでもこうやってネタになるという事は、キャッシュ用のメンバ変数一つ作るのも億劫でいくら言ってもGetComponentをUpdateとか毎フレーム通るような場所に書いちゃうGetComponentおじさんが居るのかもしれない!!!?(幸い僕の観測範囲には居ません)

ピコーン、思いついた!

そんなGetComponentおじさんのソースコードに、こんなのをこっそり張り付けるのはどうだろう!?

以下「貼るだけで速くなる」コード

貼るだけで速くなる.cs
    private Dictionary<Type,object> _componentCache = new Dictionary<Type, object>();//Cache用Dictionary
    public new T GetComponent<T>() where T : Component
    {
        var type = typeof(T);
        if (_componentCache.ContainsKey(type) == false)//Dictionaryに無ければGetしたりAddしたりする
        {
            var component = base.GetComponent<T>();
            if (component == null)
            {
                component = gameObject.AddComponent<T>();
            }

            _componentCache.Add(type, component);
        }

        return (T) _componentCache[type];//Dictionaryから返却
    }

これは、親クラスのGetComponentをnew付きでオーバーライドしているので、GetComponentおじさんが意識しなくてもこっちの処理に誘導され、勝手にDictionaryで管理されるって寸法ですよ!

ふふふ、GetComponentおじさんは今まで通り書けるし、キャッシュされるから若干速くなると思うし、WinWinだ!(GetComponentおじさんの質は上がりません)

そう思っていた時期が僕にもありました。

Profiler.CustomSamplerとの出会い

まぁ、ネタで書いたコードだったので特に実測もしてなかったんですが、そんな折テラシュールブログさんで
【Unity】独自に作成したスレッドの処理時間をCPU Profiler(Timeline)で確認する - テラシュールブログ

こんな記事が。

まぁ、この記事はスレッド処理を自前で書いた場合のProfilerでの計測の話が主ではありますが、程度の低い僕はその前の「Profiler.CustomSampler」なるものの方にひっかかりました。

「ほう・・・。 結構簡単にProfilerに表示できるんだな・・・。」

というわけで、件(くだん)の「貼るだけで速くなるコード」の効果を計測してみようじゃないか! そう思ったんです。 ・・・思ってしまったんです。

以下計測コード

計測.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;

public class GetComponentTest : MonoBehaviour
{
    private readonly Dictionary<Type, object> _componentCache = new Dictionary<Type, object>();

    public new T GetComponent<T>() where T : Component
    {
        var type = typeof(T);
        if (_componentCache.ContainsKey(type) == false)
        {
            var component = base.GetComponent<T>();
            if (component == null) component = gameObject.AddComponent<T>();

            _componentCache.Add(type, component);
        }

        return (T) _componentCache[type];
    }

    private readonly int max = 10000;
    private CustomSampler samplerNormal;
    private CustomSampler samplerDefaultGetComponent;
    private CustomSampler samplerOverrideGetComponent;
    private CustomSampler samplerCache;
    private Transform cacheTransform;

    private void Awake()
    {
        samplerNormal = CustomSampler.Create("Normal");
        samplerDefaultGetComponent = CustomSampler.Create("GetComponentおじさん");
        samplerOverrideGetComponent = CustomSampler.Create("貼るだけで速くなる");
        samplerCache = CustomSampler.Create("NormalCache");
        cacheTransform = base.GetComponent<Transform>();
    }

    private void Update()
    {
        //普通の使い方
        samplerNormal.Begin();
        for (var i = 0; i < max; ++i) transform.Rotate(0,0,Time.deltaTime);
        samplerNormal.End();

        //GetComponentおじさん
        samplerDefaultGetComponent.Begin();
        for (var i = 0; i < max; ++i) base.GetComponent<Transform>().Rotate(0,0,Time.deltaTime);
        samplerDefaultGetComponent.End();

        //貼るだけで速くなるコード(?)
        samplerOverrideGetComponent.Begin();
        for (var i = 0; i < max; ++i) GetComponent<Transform>().Rotate(0,0,Time.deltaTime);
        samplerOverrideGetComponent.End();

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

よろしくお願いしまーーーす!!!(ポチ)

image.png

・・・ん?

image.png

・・・・。 
負けとる。

負けとるがな!!! GetComponentおじさんに負けとるがな!!!! げぇぇーー。

このままじゃ終われない

まてまてまて。これはあれだ。 オーバーライドしたGetComponentの中でif文入ってるのがいけない。 if文は遅いってばっちゃも言ってた。

~~2018年5月7日追記~~
@neuecc 様からのご指摘通り、ボトルネックはif文ではなく、ContainsKeyによる存在チェックと、indexerによる値取得により2重でDictionaryを走査している事でした。
TryGetValueを使用する事で、ContainsKeyをせずにDictionaryからkeyの存在チェックとvalueの取得が同時にできるので大分高速化出来ました(ただ、それでもGetComponentおじさんには勝てませんでした)
~~追記終了~~

        //↓この必ず通ってしまうif文がボトルネックなのでは!!!?
        if (_componentCache.ContainsKey(type) == false) 
        {

よし、仕様を一部変更する!!

Dictionaryのキャッシュに登録するためのメソッドを別で用意して、オーバーライドしたGetComponentはDictionaryに登録されている前提でノーチェックで返却だ!!!

//    public new T GetComponent<T>() where T : Component
//    {
//        var type = typeof(T);
//        if (_componentCache.ContainsKey(type) == false)
//        {
//            var component = base.GetComponent<T>();
//            if (component == null) component = gameObject.AddComponent<T>();
//
//            _componentCache.Add(type, component);
//        }
//
//        return (T) _componentCache[type];
//    }

    public new T GetComponent<T>() where T : Component
    {
        return (T) _componentCache[typeof(T)];
    }

    private void AddCache<T>() where T : Component
    {
        _componentCache[typeof(T)] = base.GetComponent<T>();
    }

もちろん、いきなりGetComponentしたらKeyNotFoundException出ちゃうのでAwakeやStartでAddCacheを先に呼んでもらう事にする!!

    private void Awake()
    {
        samplerNormal = CustomSampler.Create("Normal");
        samplerDefaultGetComponent = CustomSampler.Create("GetComponentおじさん");
        samplerOverrideGetComponent = CustomSampler.Create("貼るだけで速くなる");
        samplerCache = CustomSampler.Create("NormalCache");
        cacheTransform = base.GetComponent<Transform>();

        //TransformをGetComponentするならこの1行を先に!
        AddCache<Transform>();
    }

(・・・。もうこの時点で「貼るだけで速くなる」じゃないんだよなぁ。)

今度こそ!よろしくお願いします!!(ポチ)

image.png

だーーめだ。 勝てね。Dictionary遅っせぇ!!!
はい、かいさーーん!!!ほらほら仕事戻るよーーー。

まとめ

「論より証拠なのでちゃんと計測しようね」に尽きる一件でした。

しかし・・・。
image.png

GetComponentが普通の使い方(tarnsformに直接)やCache(メンバ変数で先に確保)に比べてそんなに遅くないんですが・・・?

ここらへん、Unityのバージョンで上下したりするんですかね。僕は精魂尽き果てたのでそこまでは調べておりません。
現場からは以上です。

おまけ

Transformだけじゃなくて、SpriteRendererとかでもやってみようと思って、適当に

for (var i = 0; i < max; ++i) 
{
    GetComponent<SpriteRenderer>().color += Color.gray;
}

なんてコード書いてProfiler見たら、ガベコレが走りまくってました。

Vector3.left は内部的にはしっかり定数化されてるんだけれど、Color.から続く色定数っぽい奴は中身は

    public static Color grey
    {
      get
      {
        return new Color(0.5f, 0.5f, 0.5f, 1f);
      }
    }

こんな感じで毎回Color構造体がnewされてるんですよ! そりゃガベコレも走るわ!!

~~2018年5月7日追記~~
こちらも @neuecc 様のご指摘通り、(自分でも書いてますが)Colorは構造体なので、基本的にはGCの対象にならないためProfilerで出ていたGCはColor構造体とは無関係でした。大変申し訳ございません。
(というか、今改めてやってみたらGCなんて走ってないっぽいんですよね・・・。僕は何を見たんでしょうか。きっと頭がおかしかったんだと思います。)
~~追記終了~~

では、また。