##前置き
- 使用したUnityは2018.1.0b13です。
- ネタのようなネタじゃないような話です。
##君はGetComponentおじさんを知っているか
「僕Unityできますよ」Tシャツのコードレビュー
でも触れられていましたが、
「GetComponentは遅いからUpdate内で使うなんてトンデモナイ!」
というのが定説です。いや、定説というか事実なんですが。
それでもこうやってネタになるという事は、キャッシュ用のメンバ変数一つ作るのも億劫でいくら言ってもGetComponentをUpdateとか毎フレーム通るような場所に書いちゃうGetComponentおじさんが居るのかもしれない!!!?(幸い僕の観測範囲には居ません)
##ピコーン、思いついた!
そんなGetComponentおじさんのソースコードに、こんなのをこっそり張り付けるのはどうだろう!?
以下「貼るだけで速くなる」コード
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に表示できるんだな・・・。」
というわけで、件(くだん)の「貼るだけで速くなるコード」の効果を計測してみようじゃないか! そう思ったんです。 ・・・思ってしまったんです。
以下計測コード
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();
}
}
よろしくお願いしまーーーす!!!(ポチ)
・・・ん?
・・・・。
負けとる。
負けとるがな!!! 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>();
}
(・・・。もうこの時点で「貼るだけで速くなる」じゃないんだよなぁ。)
今度こそ!よろしくお願いします!!(ポチ)
だーーめだ。 勝てね。Dictionary遅っせぇ!!!
はい、かいさーーん!!!ほらほら仕事戻るよーーー。
##まとめ
「論より証拠なのでちゃんと計測しようね」に尽きる一件でした。
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なんて走ってないっぽいんですよね・・・。僕は何を見たんでしょうか。きっと頭がおかしかったんだと思います。)
~~追記終了~~
では、また。