Edited at

VS GetComponentおじさん

More than 1 year has passed since last update.


前置き


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

~~追記終了~~


では、また。