Update を高速にするためのマネージャーを作る

ども、ゴコーです。

今回は Update() の軽量化についてです。

スポンサーリンク

目次

  • Update は重い?
    – コルーチンをUpdateの代わりにするのは重い
  • マネージャーを作ってみる

Update は重い?

そこまで重いわけではありませんが、一度に1000個単位で動かすような状況では無視できなくなってきます。何の処理もしない空の Update() を書くだけでも、です。

そのあたりの話はすべて公式ブログの Update()を10000回呼ぶ にまとまっています。

この記事から分かることは以下のとおり。

  • Update() は Mono か IL2CPP によって「毎フレームUpdateを呼ぶべきスクリプトのリスト」に追加され、毎フレーム呼ばれる。この方法では C++ から C# のコードを呼んでいるので、空の Update() でもコストがかかる
  • 大量に Update() を書くより、マネージャーを作ってマネージャーの Update() からそれらを更新した方が断然効率がよく、高速
  • マネージャーから呼び出す場合、 List より配列の方が生成される C++ コードがシンプルになるので高速

大量にオブジェクトを呼び出す環境においては Update マネージャーを作った方がいいということが分かりますね。

コルーチンを Update の代わりにするのは重い

話はそれますが、Start() は返り値を void でなく IEnumerator にしてそのままコルーチンとして使うことができるので、以下のように Update の代わりに使うこともできます(Awake() ではできません)。

このようにコルーチンを Update() の代わりにすればメンバ変数を減らせる、ステートマシンっぽく使えるなど利点もあるのですが、大量にオブジェクトを出すのは遠慮した方がいいかもしれません。大量に出した場合、Update() の比ではないくらい重くなるからです。

実際にプロファイラーの画像を並べてみるとよく分かります。

以下は 空のUpdate() を呼ぶだけのオブジェクト、何もしないコルーチンを呼び続けるだけのオブジェクトをそれぞれ10000個ずつ呼び出したときの比較です。

グラフの水色部分がスクリプトによるの負荷を表しています。

Desktop 2016-05-23 21.45.25

まずは Update() の場合。 BehaviourUpdate というのがおそらく C++ のレイヤーから Update() 呼び出す部分でしょう。

Desktop 2016-05-23 21.51.26

次にコルーチンです。水色部分が先ほどの比ではないくらいの厚さになっているのが分かるかと思います。また、 GC Alloc が毎フレーム 322.3 KB(オブジェクトの WaitForEndOfFrame() ごとに 33B )発生しており、定期的に GC.Collect() が走ってスパイクが起こってしまいます。

これは極端な比較で、機器のスペックによっても差は出る話なので「大量のコルーチンは重いのかもしれないんだな」程度の認識だけ持ってもらえたらいいかと思います。先ほども書いた通り便利な面もあるので、全く使わない!みたいな極端なことはしなくていいかなと。

というか、後述の MicroCoroutine を使いましょう。

マネージャーを作ってみる

先ほど、 Update() ではなく Update マネージャーを作って自分で更新した方が高速であるということを書きました。ということで作ってみました。

LateUpdate() や FixedUpdate() なども使いたい、となった時のことを考えてインターフェイスを登録するようにしました。そのため登録は手動になります。

こんな感じ。

UpdateManager は大部分を @neuecc さまの Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映 及び、 MicroCoroutine を参考にさせてもらっています。そんな訳でマネージャーの方針もほぼ同じで、以下のようになりました。

  • List ではなく、配列を使う
  • 追加、削除などの度に配列の長さは変えない
    – 追加は末尾に対して行い、足らなければ配列をリサイズ
    – 削除は null を割り当てる
    – 配列整理時に配列末尾の要素を null の部分に割り当てる
  • 配列が膨らむだけなのが気になるかもしれないので減らせるようにしておく (ReduceArraySizeWhenNeed)

一応減らせるようにもしましたが、 MicroCoroutine と同様基本減らす必要はないんじゃないかなあという感じがしています。あと、こちらでは整理は一定時間ごとに実行されるのではなく、削除時に呼ばれるようにしてます。

ただし、スレッドへの考慮はしてません。

最後に、パフォーマンスの比較です。

Desktop 2016-05-23 22.04.35

Update() を素で呼ぶと 5.25 ms ほどかかっていたのが 0.20 ms まで下がっているのが分かるかと思います。機器のスペックによっても(ry なのであくまで参考程度ですが。

それにしてもプロファイラーは一定時間計測して平均を出す、みたいな使い方はできたら嬉しいのになあ…方法ご存知の方は是非教えてください!

さて、いかがだったでしょうか。

実行方法をほんの少し変えるだけでこんなに差が出るのは驚きですね。

気になった人は是非使ってみてください!

ただ、実際は UniRx 使えば万事オッケーな気がします←

では、また。

Comments

comments

スポンサーリンク
336*280px

コメント

  1. @narudesign_dev より:

    RT @backlight1144: 新しい記事です: Update を高速にするためのマネージャーを作る https://t.co/1huxVZFX0c by Unity道しるべ

  2. @Kan_Kikuchi より:

    φ(・ェ・o)メモメモ

    Update を高速にするためのマネージャーを作る https://t.co/jbWhilLPtq

  3. RT @backlight1144: 新しい記事です: Update を高速にするためのマネージャーを作る https://t.co/1huxVZFX0c by Unity道しるべ

  4. @mamokun0123 より:

    RT @backlight1144: 新しい記事です: Update を高速にするためのマネージャーを作る https://t.co/1huxVZFX0c by Unity道しるべ

  5. @youri_ssk より:

    RT @backlight1144: 昨日書いた記事です|Update を高速にするためのマネージャーを作る https://t.co/sQnEe698QK

  6. @rosiro より:

    Update を高速にするためのマネージャーを作る https://t.co/wxItTXk5Am

  7. @yusekisekine より:

    Update を高速にするためのマネージャーを作る https://t.co/4rW9i5BDmQ
    メモ。