ゲームを作っているうちに、何か重い的な事もあるかもしれません。そんな時、プロファイラを使用して重い部分を発見・改善する必要があります。
Unityの標準プロファイラはXcodeのInstrumentsよりも精度で劣りますが、サクっと確認する上では結構使い勝手の良い機能です。あとWindowsでも動きます。
今回はこのプロファイラを使用して、パフォーマンスのボトルネックを探す方法についてメモします。今回は「追う方法」についてです。
パフォーマンスの現状を知る
まずは現状のパフォーマンスを把握する所から始めます。
UnityのメニューバーWindow>Profilerを選択するとProfilerのウィンドウが開きます。
CPU負荷の全体像を知る
次にゲームを再生すると、プロファイラの中身がどんどん変化していきます。
CPUプロファイラのグラフの見方はこんな感じです。
凄くざっくりと言えば、縦線が負荷で横線がフレーム(時間)です。
つまり縦線グラフが目標のFPSラインを上回る箇所があれば、そこで処理落ちしている訳です。それが全体的にであれば抜本的な解決が、部分的であれば何らかの条件で大きな負荷がかかっています。
表示/非表示の設定は、この際のノイズを取り除くのに役立ちます。例えば、スクリプト負荷を確認したいケースでは、Vsync(■FPSが60フレームに一致するようにCPUを止める)やPyhsics(■物理演算)、Rendering(■レンダリング負荷)といった処理を除いた、純粋なScript(■スクリプトの負荷)を確認しやすくします。
例えば下の内容であれば、30FPSを割った原因はスクリプトにあると言えそうです。
なお勘違いしやすいですが、VSync■は負荷ではなく只の処理待機時間です。これは良い感じにフレームを合わせてる時間なので、CPU負荷とは考えない方が良いです。
フレーム落ちするような処理の理由を知る
次に負荷が高い理由を知ります。
この際に個人的にお気に入りなのがTimelineの利用です。一覧の少し下にHierarchyとなっている項目があるので、それをTimelineに変更します。
あとは負荷が高い所を選択(ゲームが一時停止になります)すると、どんな処理に時間がかかっているのかを調べる事が出来ます。
この画面は、横線が処理時間です。つまり、長い処理はまずいです。
で積み上がっているバーですが、これは「上から順に、メソッドの呼び出し状況」です。下の図を例とすると、こんな感じに読めます。
- Loading.UpdatePreloadingが86ms
- そのうち、ApplicationIntegrateAssets in backgroundが86ms
- そのうち、Prefab.MergePrefabの処理が62ms、小さくて見えないけどLoading.AwakeFromLoadが7msで、残りの7msがApplicationIntegrateAssets in background自身の処理
- 以下略
こんな感じで、明確にボトルネックがある場合は横線に伸びてる所を探していけば、結構何がボトルネックなのか見つけやすくなってます。
まぁ今回の場合はシーンのロードなので割とどうしようもない感はありますが…
Main Thread以外の、例えばRender ThreadやUnity Job Systemは、メインスレッドを止めない別スレッドの処理です(※ユーザーの作ったスレッドは表示されません)。
ここではCanvasの再構築とかParticleの計算とかレンダリングとか、そういった処理の負荷を確認出来ます。
各スレッド間で同期を取るため、ココで負荷がかかりすぎるのは良くないのですが、処理が間に合っている限りは処理時間がかかっていてもゲームの進行に余り負荷を与えなかったりするので、別枠として考える必要がありそうです。
処理に時間がかかっている所を探す
ボトルネックの場合はTimelineが楽ですが、全体的に負荷を改善したい場合はどうでしょう。その場合はtimelineではなくHierarchyを使用します。
Hierarchyの場合、各々の処理がCPU100%のうち何%を使用しているかを確認する事が出来ます。また、それが何秒かかっているのか…等も確認出来る感じです。
つまり、Totalのタブを選択してフレームを左右に移動し、常にCPU喰ってる奴(Wait for targe fps=vsyncを除く)を見つけ出し排除すれば、全体的に付加がかかっている処理を減らす事に繋がります。
少しややこしいのがTotalとSelfで、Totalは該当のメソッドが呼ばれた時の全体的な負荷、Selfがメソッド自身の負荷です。
例えば下のようなケースでは、SelfはStart内のFor文の処理を、Totalの場合はDebug.Logまで含めた負荷を計上します。
なお下の例は「但し」が付きます。詳細は下のDeep Profileで。
これで、余計な処理がかかっている部分を見つけ出し排除していきます。
DeepProfileとProfiler.BeginSampleの使い分け
UnityのプロファイラはUnity標準の幾つかのAPI、及びMonobehaviourのUpdateやStart・Coroutine等の処理しか計上してくれません。
そこでもっと深く処理の内情を知りたい場合に使えるのがDeepProfileとProfiler.BeginSample/EndSampleです。
Deep Profile
まずDeep Profileですが、プロファイラのDeep Profileにチェックを入れて使用します。
Deep Profileを使用すると、Unity標準の幾つかのAPIだけでなく、もっと深い階層のAPI負荷も見れるようになります。
下は先ほどのHogeクラスの中身をプロファイルした結果です。DeepProfileを設定する前はStartの中身について言及されていませんでしたが、Deep Profileに設定するとSampleMthodどころかDebugLogの負荷も確認出来ます。文字列結合死すべき慈悲は無い。
ちなみに、上のSelfとTotalの比較にてSampleMethodをSelfから排除していますが、これはDeepProfileのケースです。Deep Profileを設定していない場合は、SampleMethodもStartの負荷として計上されます。
ちなみに、この設定はTimelineにも影響します。
Profile.BeginSample/EndSample
もう一つのProfilerに負荷を計上するアプローチとして、Profiler.BeginSampleとEndSampleがあります。
これは、BeginSample~EndSampleで囲った処理を負荷としてプロファイラに載せるという物です。
これの特徴は2つ、一つはDeep Profileと比較して動作が軽い事、もう一つは異なるスタックトレースの処理を一つにまとめて計上する事が出来る事です。
下はSampleMethodが異なる呼出順番で複数回呼ばれている場合の差です。Deep Profileを使用した場合は呼出順に従い負荷を計上していますが、Profile.sampleシリーズで出した場合は、一つにまとめ「2回呼出」があったことにしています。
これを良いと見るか悪いと見るかは人それぞれですが、良いと見た場合はメソッドの呼出回数やトータルの負荷を確認しやすくなります。
なお、一つにまとめた上で異なる内容として確認したい場合は、Raw Hierarchyを使用します。これで、何度も呼ばれている内の一つが重い…みたいなケースを探す事が出来そうです。
長くなってきたので分割します。