41
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

VS GetComponentおじさん

Last updated at Posted at 2018-05-02

##前置き

  • 使用した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なんて走ってないっぽいんですよね・・・。僕は何を見たんでしょうか。きっと頭がおかしかったんだと思います。)
~~追記終了~~

では、また。

41
22
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?