Unity における最適化
メモリ

プロファイリング

パフォーマンスについて考える時に理解する必要があるのは、最適化の試みは全て調査、発見のプロセスから始めなければならないということです。まず最初に行うべきは、アプリケーションをプロファイリングしてホットスポット (問題箇所) を特定することです。次にそのプロファイリング結果を、プロジェクトの技術的構造やアセット構造と照らし合わせて分析します。

ノート: このセクション内で言及されている、ネイティブコードのプロファイリング出力内のメソッド名は、 Unity 5.3 時点での名前です。今後のバージョンの Unity では、これらのメソッド名は変更される可能性があります。

ツール

Unity で開発を行うディベロッパーは、様々なプロファイリング用ツールを使用することができます。 Unity ビルトインのツールには CPU ProfilerMemory Profiler 、新しい 5.3 Memory Analyzer などがあります。

ただし基本的に、最も有益なデータは各プラットフォーム専用のツールから得られます。以下はそのいくつかの例です。

  • iOS 向け - InstrumentsXCode Frame Debugger

  • Android 向け - Snapdragon Profiler

  • Intel CPU/GPU を実行するプラットフォーム向け - VTuneIntel GPA

  • PS4 向け - Razor パッケージ、VR Trace

  • Xbox 向け - Pix ツール

基本的に、これらのツールは通常は、IL2CPP を使ってプロジェクトの C++ バージョンを作成することのできるプラットフォームで最も有効です。これらのネイティブコード版は、 Mono 下で実行されている場合には利用不可能な、 トランスペアレントなコールスタックと高分解能なメソッドのタイミングを提供します。

Instruments を使用した iOS ゲームのプロファイリングに関するガイドは、Unity から既に提供されています。Profiling with Instruments を参照してください。

起動時の出力の分析

起動時間のプロファイリング出力を確認する場合、主に調査すべきメソッドは 2 つあります。プロジェクトの設定、アセット、コードが起動時間への影響に関しては、主にこの 2 つのメソッドが原因となります。

(注) 起動時間の表現はプラットフォームによって異なります。ほとんどのプラットフォームでは静止スプラッシュ画面が表示されます。

上の画像は、 iOS デバイスで実行したサンプルプロジェクトの、 Instruments の出力のスクリーンショットです。プラットフォーム別の startUnity メソッド内の、 UnityInitApplicationGraphics メソッドと UnityLoadApplication メソッドにご注目ください。

UnityInitApplicationGraphics は多くの内部機能を実行します。例えばグラフィックス デバイスの設定や、多くの Unity 内部システムの初期化などです。また、 Resources システムの初期化も行います。メソッドはこれを行うためには Resources システムに含まれる全てのファイルのインデックスを読み込む必要があります。

“Resources” という名前のフォルダー内の全てのアセットファイル(1) ([注] プロジェクトの “Assets” フォルダー内にある “Resources” フォルダーおよび、それら “Resources” フォルダー内の全ての子フォルダーに限る)は、 Resource システムのデータに含まれます。したがって、 Resources システムの初期化に掛かる時間は、 “Resources” フォルダー内のファイル数に応じて、少なくとも直線的に増加します。

UnityLoadApplication にはプロジェクトの最初のシーンを読み込んで初期化する(複数の)メソッドが含まれます。これには最初のシーンの表示に必要な全てのデータのデシリアライゼーションとインスタンス化([例]シェーダーのコンパイリング、テクスチャのアップロード、ゲームオブジェクトのインスタンス化)が含まれます。また、最初のシーン内の全ての MonoBehaviour の Awake コールバックが、この時点で実行されます。

上記のようなプロセスになっているということは、つまり、プロジェクトの最初のシーン内の Awake コールバック内に長時間掛かるコードがひとつでもある場合、そのコードが原因でプロジェクトの起動時間が長くなっている可能性があるこということです。これを解決するには、時間の掛かるコードを削除するか、そのコードをアプリケーションのライフサイクル内の他の場所で実行するようにする必要があります。

ランタイムの出力の分析

起動時間の後にキャプチャーされた出力のプロファイリングに関しては、主に注目すべきは PlayerLoop メソッドです。これは Unity のメインループで、中にあるコードが 1 フレームに 1 回実行されます。

上の画像は Unity 5.4 のサンプルプロジェクトのプロファイリングから取ったスクリーンショットですが、 PlayerLoop 内で最も興味深いメソッドのいくつかを確認することができあます。(注) PlayerLoop 内のメソッドの名前は Unity のバージョンによって異なる可能性があります。

PlayerRender は、 Unity のレンダリングシステムを実行するメソッドです。これにはオブジェクトのカリング、動的バッチの計算、 GPU への描画指示のサブミットなどが含まれます。イメージエフェクトや、レンダリングベースのスクリプト コールバック( OnWillRenderObject など)は全てここで実行されます。基本的に、プロジェクトがインタラクティブな時に最も CPU 時間を消費するのがこのメソッドです。

BaseBehaviourManager はテンプレート化された 3 つのバージョンの CommonUpdate を呼び出します。これらは、現在のシーン内のアクティブなゲームオブジェクトに添付された MonoBehaviour 内の特定のコールバックを実行します。

  • CommonUpdate<UpdateManager>Update コールバックを呼び出します。

  • CommonUpdate<LateUpdateManager>LateUpdate コールバックを呼び出します。

  • CommonUpdate<FixedUpdateManager> は、物理システムにチェックマークが入っている場合に FixedUpdate を呼び出します。

調査する上で基本的に最も注目すべきメソッドファミリは BaseBehaviourManager::CommonUpdate<UpdateManager> です。 これがUnity プロジェクト内で実行される大部分のスクリプトコードのエントリーポイントだからです。

この他にも注目すべきメソッドがいくつかあります。

UI::CanvasManager は、プロジェクトが Unity UI を使用している場合に、いくつかのコールバックを実行します。これには Unity UI のバッチ計算やレイアウト更新が含まれます。プロファイラーに CanvasManager が表示される場合、その原因として最もよく見られるのは、この 2 つの処理です。

DelayedCallManager::Update はコルーチンを実行します。これに関しては、このドキュメントの「コルーチン」のセクションで詳しく解説しています。

PhysicsManager::FixedUpdate は PhysX 物理システムを実行します。これは主に PhysX の内部コードを実行することによって行われ、現在のシーン内にある物理オブジェクト(リジッドボディやコライダーなど)の数に影響されます。ただし、ここには物理ベースのコールバックも表示されます ― 具体的には OnTriggerStayOnCollisionStay です。

プロジェクトが 2D 物理演算を使用している場合、それは、一式の類似した呼び出しとして Physics2DManager::FixedUpdate の下に表示されます。

スクリプトメソッドの分析

スクリプトが IL2CPP を用いてクロスコンパイルされてプラットフォーム上で実行されている場合、出力内で ScriptingInvocation オブジェクトの含まれるラインを探してください。そこが、 Unity の内部ネイティブコードが、スクリプトコードを実行するためにスクリプト ランタイムへ遷移している箇所です。(2)([注] IL2CPP 経由で実行された後は、 C#/JS スクリプトコードもネイティブコードとなります。ただしこのクロスコンパイル コードは、主に IL2CPP ランタイム フレームワークでメソッドを実行しており、手書きの C++ とはそれほど類似しません。)

上の画像も、 Unity 5.4 で実行されたサンプルプロジェクトのプロファイリング出力のスクリーンショットです。 RuntimeInvoker_Void ラインの下に入れ子になったメソッドは全て、 1 フレーム毎に 1 回実行されるクロスコンパイル済 + C# スクリプトの一部です。

これらの出力ラインの読み取り方は比較的簡単です。各ラインとも、元のクラス名、下線、元のメソッド名の順で連なった形になっています。このサンプルの出力では、 EventSystem.UpdatePlayerShooting.Update、その他いくつかの Update メソッドが確認できます。これらは、ほとんどの MonoBehaviour 内に見られる、 Unity の標準の Update コールバックです。

メソッドを展開すると、その中のどのメソッドが CPU 時間を消費しているか確認できます。これは、プロジェクト内のその他のメソッド、 Unity API や C# ライブラリ コード内のメソッドに関しても同様に行えます。

上の出力からは、 StandaloneInputModule.Process メソッドが 1 秒に 1 回、 UI 全体にレイキャストを行っていることが確認できます。これは、何らかのタッチイベントがUI 要素上をホバリングしたり要素をアクティベートしたりしているかどうか検知するためです。 これに掛かる負荷の大部分は、各 UI 要素全てに対する反復処理と、マウスの位置が長方形の境界内に収まっているかどうかのテストによるものです。

アセットの読み込み

アセットの読み込みは CPU のプロファイリング出力内でも特定できます。アセットの読み込みを示す主要なメソッドは SerializedFile::ReadObject です。このメソッドは(特定のファイルからの)バイナリデータ ストリームを、 Transfer という名前のメソッドによって実行される Unityのシリアライゼーション システムに接続します。 Transfer メソッドは、テクスチャー、 MonoBehaviour、 パーティクルシステムを始めとする全てのアセットタイプに見られます。

上のスクリーンショットでは、シーンが 1 つ読み込み中です。これが行われるためには、そのシーン内の全てのアセットが Unity によって読み出され、デシリアライズされる必要があります。 SerializedFile::ReadObject の下の各種 Transfer メソッドがそれを示しています。

一般的には、ランタイム中にパフォーマンスのカクつきが見られ、多くの時間が SerializedFile::ReadObject によって費やされていることがパフォーマンス出力に示される場合、アセットの読み込みのためにフレームレートが低下していることを意味します。(注)ほとんどの場合、 SerializedFile::ReadObject がメインスレッドで確認できるのは、アセットの同期的読み込みが SceneManagerResources か AssetBundle API によってリクエストされた場合のみになります。

この種のパフォーマンスのカクつきは一般的な方法で緩和できます。アセットの読み込みを非同期にする(重い ReadObject コールがワーカースレッドに移される)か、一部の重いアセットを事前に読み込んでください。

Transfer コールは、オブジェクトをクローン(複製)する場合にも現れます(出力内では CloneObject メソッドがそれに当たります)。 Transfer への呼び出しが CloneObject コールの下に表示される場合、該当アセットがストレージから呼び出されておらず、その代わりに古いオブジェクトのデータが新しいオブジェクトに送信されていることを意味します。 Unity はこれを行うために、古いオブジェクトをシリアライズし、その結果のデータを新しいオブジェクトとしてデシリアライズします。

脚注

  • (1) プロジェクトの “Assets” フォルダー内にある “Resources” フォルダーおよび、それら “Resources” フォルダー内の全ての子フォルダーに限ります。
  • (2) IL2CPP 経由で実行された後は、 C#/JS スクリプトコードもネイティブコードとなります。ただしこのクロスコンパイル コードは、主に IL2CPP ランタイム フレームワークでメソッドを実行しており、手書きの C++ とはそれほど類似しません。

Unity における最適化
メモリ