【Unity】CPUプロファイラでパフォーマンスを改善する 後編
CPUプロファイラに関する内容の後編です。
パフォーマンスの問題を解決する
パフォーマンスの問題はケースバイケースです。銀の弾丸が無い以上、実際にやってみて効果を確認する以外には無い感じであはあります。
また、一つの事柄に注目しすぎるのも問題です。例えばAnimatorはAnimationを比較するとAnimatorのほうが若干負荷が高いですが、SkinedMeshRendererの変形コストも加算するとAnimatorの方が負荷が低かったりします。
今回はそういった細々とした内容ではなく、大抵のゲームでの敵・GC、それとオーバーヘッドについてでです。
GCを減らす
C#を使用していて何ですが、Unityで割と問題になりやすいのはGCについてです。特に巨大になってしまったゴミ溜めを収集する際には、非常に大きなスパイクを起こしやすいです。
でGCの発生ですが、CPUプロファイラのGC Allocにて確認出来ます。
リソースのロード中ならば仕方がないのですが、実行中は出来る限りGCを引き起こさないようにしたい所です。
GCの発生要因を減らす
GCはヒープ領域に確保された使い捨てのメモリが引き起こします。凄くざっくりと言うならば、クラスや配列といった物です。
よく言われるGCを引き起こす物としては、こんな感じの物があります。
これらは正直「使えると便利な機能」なのでできるだけ使いたい所ですが、パフォーマンスの事を考えるならば、可能な限り0にすることが望ましいです。
少なくとも毎フレーム作るような事は避けたほうが良さそうな感じです。この辺りは手間と読みやすさとパフォーマンスのトレードオフなので、ケースバイケースで。
色々な人から各々の対策を聞きかじると、大体こんな感じになりそうです。
- LINQ
→使わない。 - ラムダ式
→問題が発生しにくい物を使う or 使わない。 - クラスや配列のnew
→使い回す。動的に作るのはできるだけ避ける。
フィールドに定義して使い回す。
きりの良いタイミングで作って使い回す。
クラスは構造体にしてrefを使用。
幾つかのAPIは配列の戻り値よりもListの引数を使う(Listは使い回す前提) - foreach
→forに変換(monobehaviourで一発変換)
.NET 4.xで発生しなくなった(?) - coroutine
→updateで行う。もしくは近いものを自作(!?) 将来的にはGCを無くすとかなんとか。
※Unity 5.3.5p2からコルーチンはGCを起こさなくなりました。 - ボックス化
→キャストは避ける
.NET 4.xで発生しなくなった(?) - 文字列結合
→StringBuilderを使う。StringBuilder自体も使い回す。 - 文字列の利用、文字列への変換、文字列のry
→const stringを使用(ビルド時間伸びるかも)
シェーダーやAnimatorへのアクセスはハッシュを取得してキャッシュ。 - OnGUI
→デバッグ用。殺すべし。
無理に回避しようとすると非常に読みにくいコードになる事もあるので、どうするかは使用頻度や負荷とご相談で。
コルーチンとGC
以前(Unity 5.3.5p1以前)はコルーチンを使用すると1回の呼出につき数バイトのゴミが発生していましたが、5.3.5p2以降はゴミが発生しなくなりました。
但しコルーチン自体はallocateを発生させなくなりましたがコード上でnewを記述している場合は当然の如くGCが走ります。
例えば、よくある記述方法ですが、yield return new waitforseconds のようにコルーチン内で記述すると、毎回呼び出される毎に作られるのでゴミになります。
ループの外にて生成すると、ゴミにはなりません。
GCの負荷を減らす
GCの負荷ですが、実は複雑な参照関係をもつと負荷が上がるみたいです。この辺りはUnite 2015のUnity パフォーマンス・チューニングでも紹介されています。
例えば下の図のように、クラスで定義した場合と構造体で定義した、構造的には似たようなオブジェクトがあります。
この2つのオブジェクトを100万個作って破棄した際のGCは、資料によるとこのようになるそうです。
- class:35ms
- struct :10ms
また、この2つの構造の中からstringを抜いた場合、さらに負荷が減少します。文字列分のGCが減ってるのでフェアでは無いような気もしますが。
- class:20ms
- struct:0.18ms
また事前拡張を避ける事でもGCの負荷は減らせるそうですが、詳細不明。
これは微妙に違う気がしなくも(1面として影響はありそうですが)
オーバーヘッドとは何ぞや
プロファイラを見ると、Othersに含まれるOverheadという物が目につきます。これはUnity knowladge曰く「総フレームタイム以外の時間」だそうです。要するに細かく計測していない全てという感じになりそうです。
この項目は大抵の場合、Unityが計測していないエンジンのサブシステムが関連しているそうです。例えばオーディオやらスプライト表現やら、物理演算やら、パーティクルやらオクルージョンかリング等々の細かい機能群。つまり、使用してる機能の数が多ければ多いほど増えてく感じになりそうです。
また、iOSのメモリ不足警告等が出た場合もこちらに含まれるそうです。
実機で動作するアプリのパフォーマンス検証
ゲームのパフォーマンスを検証する場合、できれば実機(実際の機材)で確認する事が望ましいです。特にiOS/Androi、及びゲーム機といった低スペックを特化した性能で補っているタイプのプラットフォーム向けに開発する場合、このパフォーマンス特性の違いから、思ってない所で思いのほかパフォーマンスが出ないといった事が多々あります。
Unityエディタは「特定のデバイスをエミュレートしている」のではなく「同じように動作するAPIを提供している」だけなのです。
出力したアプリを計測するメリット
Unityエディタ上ではなく出力したアプリを計測するメリットは2つあります。それは上で述べる通り実機上でのパフォーマンスを確認できる点ですが、もう一つUnityエディタが作るパフォーマンスのノイズを隠す事が出来る点という物があります。
このノイズとは、例えば「#if UnityEditor」等の本来ゲームに含まれないはずの処理がゲーム内に含まれている場合、エディタ上で実行すると動作負荷として計上されてしまう事を隠せる点です。
ノイズが特に顕著なのはメモリプロファイルですが、CPUプロファイルでも実は結構あります。自分はiOS/Androidアプリの性能を検証する場合でも、とりあえずStandaloneにビルドしてパフォーマンスを検証する…といった事はよくやります。
実機で動作するアプリにプロファイラを繋ぐ
実際にアプリケーションにビルドしたアプリケーションに繋いでみます。
このプロファイルを実機に接続する最低条件は「Development Build」にチェックが入っている事です。
プロファイラとアプリの接続には、同一のWifi、もしくはプラットフォームによってはadbやusbといった接続方法を使用するみたいです。
ちなみにAndroidの場合、USB刺してadb繋ぐ事自体に負荷が発生するみたいです。
実機にてプロファイル結果を計測して取得する
Profiler.AddFramesFromFileでプロファイリングの結果をファイルに出力出来ます。
ただ負荷はUSB刺してプロファイルするのと余り変わらないです。負荷テスト中に重くなる箇所で動かせるようにするのが良いんじゃないかなと。
参考
neue cc - Unityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方
Unite 2016 /DAY1_1330_Room1_HarknessDundore_Long_Big.pdf