VS GetComponentおじさん

##前置き
- 使用したUnityは2018.1.0b13です。
- ネタのようなネタじゃないような話です。

##君はGetComponentおじさんを知っているか
[「僕Unityできますよ」Tシャツのコードレビュー](https://qiita.com/YamadaGames/items/cde2162b52ba80bc0beb)

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

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

##ピコーン、思いついた！
そんなGetComponentおじさんのソースコードに、こんなのをこっそり張り付けるのはどうだろう！？

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

```cs:貼るだけで速くなる.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)で確認する - テラシュールブログ ](http://tsubakit1.hateblo.jp/entry/2018/05/01/221240)

こんな記事が。

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

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

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

以下計測コード

```cs:計測.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](https://qiita-image-store.s3.amazonaws.com/0/37184/410aa327-8cc3-3b4c-7ff1-9dee38aa1fe7.png)

・・・ん？

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/babef6b3-2d3f-7ff1-d7b6-0b9e773a5f61.png)

・・・・。　
負けとる。

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

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

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

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

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

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

```cs
//    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を先に呼んでもらう事にする！！

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

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

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

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/c5f3eb8b-4b3a-05ae-e6a6-e72f54d536fa.png)

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

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

しかし・・・。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/c5f3eb8b-4b3a-05ae-e6a6-e72f54d536fa.png)

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

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


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

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


では、また。