Unityのメモリプロファイリング
幅広いプラットフォームやデバイス向けにゲームのパフォーマンスをプロファイリングし、磨き上げることで、プレイヤーベースを拡大し、成功の可能性を高めることができます。
このページでは、Unityでアプリケーションのメモリ使用量を分析するための2つのツール、組み込みのMemory Profilerモジュールと、プロジェクトに追加できるUnityパッケージのMemory Profilerパッケージに関する情報を提供します。
ここに掲載されている情報は、無料でダウンロードできる電子書籍「Ultimate guide to profiling Unity games」から抜粋したものです。この電子書籍は、ゲーム開発、プロファイリング、最適化に関する社内外のUnity専門家によって作成されました。
Unityのメモリプロファイリングについて学びましょう。
メモリ・プロファイリングは、ハードウェア・プラットフォームのメモリ制限に対するテスト、ロード時間やクラッシュの減少、古いデバイスとのプロジェクトの互換性確保に役立ちます。また、実際にメモリ使用量を増やすような変更を加えることで、CPU/GPUのパフォーマンスを向上させたい場合にも関係することがあります。ランタイムのパフォーマンスとはほとんど関係がない。
Unityでアプリケーションのメモリ使用量を分析するには、2つの方法があります。
Memory Profilerモジュール:これは組み込みのプロファイラー・モジュールであり、アプリケーションがどこでメモリーを使用しているかについての基本的な情報を提供する。
Memory Profiler のパッケージこれはあなたのプロジェクトに追加できるUnityパッケージです。UnityエディタにMemory Profilerウィンドウが追加され、アプリケーションのメモリ使用状況をより詳細に分析することができます。スナップショットを保管して比較することでメモリリークを見つけたり、メモリレイアウトを確認してメモリの断片化の問題を見つけたりできます。
これらの内蔵ツールを使えば、メモリ使用量を監視し、メモリ使用量が予想以上に多いアプリケーションの領域を特定し、メモリの断片化を発見して改善することができます。
マルチプラットフォーム開発では、ターゲット・デバイスのメモリ制限を理解し、予算を立てることが重要です。シーンやレベルをデザインするときは、各ターゲットデバイスに設定されたメモリバジェットを守ってください。制限とガイドラインを設定することで、各プラットフォームのハードウェア仕様の範囲内でアプリケーションがうまく動作するようにすることができます。
デバイス・メモリの仕様は、開発者向けドキュメントに記載されています。例えば、Xbox Oneコンソールは、ドキュメントによると、フォアグラウンドで実行されるゲームのために利用可能な最大メモリは5GBに制限されている。
また、メッシュやシェーダーの複雑さ、テクスチャ圧縮などのコンテンツバジェットを設定するのにも便利です。これらはすべて、メモリの割り当て量に影響する。これらの予算数値は、プロジェクトの開発サイクル中に参照することができる。
物理的RAM制限の決定
各ターゲット・プラットフォームにはメモリ制限があり、それを知れば、アプリケーションのメモリ予算を設定することができる。メモリプロファイラを使ってキャプチャスナップショットを見る。ハードウェアリソース(上の画像を参照)には、物理ランダムアクセスメモリ(RAM)とビデオランダムアクセスメモリ(VRAM)のサイズが表示されます。この数字は、そのスペースがすべて使用できるわけではないことを考慮していない。しかし、この数値は、仕事を始める際の目安となる。
ここに表示されている数字が必ずしも全体像を示しているとは限らないので、ターゲット・プラットフォームのハードウェア仕様をクロスリファレンスするのは良い考えだ。デベロッパーキットのハードウェアの方がメモリが多い場合もありますし、ユニファイド・メモリ・アーキテクチャーのハードウェアを使用している場合もあります。
サポートするプラットフォームごとに、RAMのスペックが最も低いハードウェアを特定し、これをメモリ予算の決定の指針としてください。その物理メモリのすべてが使用できるわけではないことを忘れないでほしい。例えば、ゲーム機では、古いゲームをサポートするためにハイパーバイザーが動作している可能性があり、そのために総メモリの一部を使用している可能性がある。使用するパーセンテージ(例えば全体の80%)を考える。モバイル・プラットフォームについては、より高い品質と機能を持つハイエンド・デバイスに対応するため、仕様を複数の階層に分けることも検討できるだろう。
メモリ予算が決まったら、チームごとにメモリ予算を設定することを検討する。例えば、環境アーティストはレベルやシーンがロードされるごとに一定量のメモリを割り当てられ、オーディオチームは音楽やサウンドエフェクトのためにメモリを割り当てられます。
プロジェクトが進むにつれて、予算に柔軟性を持たせることが重要だ。あるチームが予算を大幅に下回った場合、その余剰分を別のチームに割り当てる。
ターゲットとするプラットフォームのメモリバジェットを決定し、設定したら、次のステップは、プロファイリングツールを使用して、ゲームのメモリ使用量を監視し、追跡することです。
Memory Profilerモジュールには2つのビューがある:シンプルかつ詳細。アプリケーションのメモリ使用量のハイレベルなビューを取得するには、Simpleビューを使用します。必要であれば、詳細ビューに切り替えてさらに掘り下げる。
シンプル
Total Reserved Memoryの数字は、"Total Tracked by Unity Memory "である。Unityが予約しているが今は使っていないメモリも含まれる(この数字はTotal Used Memory)。
システム使用メモリの数値は、OSがアプリケーションによって使用されているとみなすものです。この数値が0と表示される場合は、プロファイラ・カウンタがプロファイリング対象のプラットフォームに実装されていないことを示します。この場合、最も頼りになる指標は総予約メモリである。このような場合、詳細なメモリ情報を得るために、ネイティブ・プラットフォームのプロファイリングツールに切り替えることも推奨される。
実行ファイル、DLL、Mono仮想マシンによって使用されるメモリ量を調べるには、フレームごとのメモリ量では不十分です。詳細なスナップショット・キャプチャを使用して、このようなメモリの内訳を調べます。
注:Memory ProfilerモジュールのDetailedビューの参照ツリーには、Native参照のみが表示されます。UnityEngine.Objectを継承した型のオブジェクトからの参照は、管理されているシェルの名前で表示される可能性があります。しかし、その下にネイティブ・オブジェクトがあるために表示されるだけかもしれない。必ずしも管理されたタイプが見られるとは限らない。Texture2Dをフィールドの1つに持つオブジェクトを例にしてみましょう。このビューを使っても、どのフィールドがその参照を保持しているかはわからない。このような詳細については、メモリー・プロファイラー・パッケージを使用してください。
メモリ使用量がプラットフォームのバジェットに近づき始める時期を大まかに判断するには、以下の「ナプキンの裏」計算を使う:
システム使用メモリ(またはシステム使用メモリが0の場合は合計予約メモリ)+未追跡メモリの大まかなバッファ/プラットフォーム合計メモリ
この数値がプラットフォームのメモリ予算の100%に近づき始めたら、Memory Profilerパッケージを使って原因を突き止めよう。
Memory Profilerモジュールの機能の多くは、Memory Profilerパッケージに取って代わられましたが、メモリ解析作業を補完するために、まだこのモジュールを使うことができます。
例:
- GCの割り当てを見抜くこと:これらはモジュールに表示されますが、Project AuditorやDeep Profilingを使えば簡単に追跡できます。
- ヒープのUsed/Reservedサイズを素早く見るには
- シェーダーメモリ解析
メモリ予算を設定する際は、ターゲットとするプラットフォーム全体のスペックが最も低いデバイスでプロファイリングすることを忘れないでください。メモリ使用量を注意深く監視し、目標制限を念頭に置く。
通常、多くのメモリが利用可能な強力な開発者システムを使用してプロファイルを作成したいでしょう(大容量のメモリスナップショットを保存したり、スナップショットを素早くロードして保存したりするためのスペースが重要です)。
メモリーのプロファイリングは、CPUやGPUのプロファイリングとは異なり、メモリーのオーバーヘッドが発生する可能性があります。より上位の(より多くのメモリを搭載した)デバイスでは、メモリのプロファイリングが必要になるかもしれないが、特に下位のターゲット仕様のメモリバジェット制限に注意すること。
メモリ使用量をプロファイリングする際の注意点:
- 品質レベル、グラフィックス階層、AssetBundleのバリアントなどの設定は、より強力なデバイス上で異なるメモリ使用量になる可能性があります。例:
- Quality LevelとGraphicsの設定が、シャドウマップに使用されるRenderTexturesのサイズに影響する可能性がある。
- 解像度のスケーリングは、スクリーンバッファ、RenderTextures、およびポストプロセッシングエフェクトのサイズに影響を与える可能性があります。
- テクスチャの品質設定は、すべてのテクスチャのサイズに影響する可能性があります。
- 最大LODはモデルなどに影響する可能性がある。
- HD(High Definition)バージョンとSD(Standard Definition)バージョンのようなAssetBundleのバリアントがあり、デバイスの仕様に基づいてどちらを使用するかを選択する場合、プロファイリングするデバイスによってアセットサイズが異なる可能性があります。
- ターゲットデバイスの画面解像度は、後処理効果に使用されるRenderTexturesのサイズに影響します。
- デバイスのサポートされているグラフィックスAPIは、APIによってサポートされているかどうかに基づいて、シェーダーのサイズに影響を与える可能性があります。
- 異なる品質設定、グラフィック階層設定、およびアセットバンドルバリエーションを使用する階層システムを持つことは、より幅広いデバイスをターゲットにすることができる素晴らしい方法です。例えば、4GBのモバイルデバイスにはアセットバンドルの高解像度バージョンをロードし、2GBのデバイスには標準解像度バージョンをロードします。ただし、上記のメモリ使用量のばらつきを念頭に置き、両方のタイプのデバイス、および異なる画面解像度やサポートされているグラフィックスAPIを持つデバイスをテストするようにしてください。
注:Unityエディタは、エディタとプロファイラからロードされる追加オブジェクトのため、一般的に常に大きなメモリフットプリントを表示します。アセットバンドル(Addressablesシミュレーションモードによる)やスプライト、アトラス、またはインスペクタに表示されているアセットなど、ビルドではメモリにロードされないアセットメモリが表示されることもあります。リファレンス・チェーンのいくつかは、エディターではより分かりにくいかもしれない。
Memory Profilerは現在Unity 2019 LTS以降のプレビュー版ですが、Unity 2022 LTSで検証される予定です。
Memory Profilerパッケージの大きな利点の1つは、(Memory Profilerモジュールが行うように)ネイティブ・オブジェクトをキャプチャするだけでなく、Managed Memoryを表示したり、スナップショットを保存して比較したり、メモリ使用量の視覚的なブレイクダウンでメモリの内容をさらに詳しく調べたりできることです。
スナップショットは、エンジン内のメモリ割り当てを表示し、過剰または不要なメモリ使用の原因を迅速に特定し、メモリリークを追跡し、またはヒープの断片化を確認することができます。
Memory Profilerパッケージをインストールしたら、Window > Analysis > Memory Profilerの順にクリックして開きます。
Memory Profilerのトップメニューバーでは、プレーヤーの選択対象を変更したり、スナップショットのキャプチャやインポートを行うことができます。
注:ターゲット選択ドロップダウンでリモート・デバイスにメモリ・プロファイラを接続して、ターゲット・ハードウェアのメモリをプロファイルします。Unityエディタでのプロファイリングでは、エディタや他のツールで追加されるオーバーヘッドにより、不正確な数値が得られます。
Memory Profiler ウィンドウの左側には Workbench エリアがあります。保存されたメモリースナップショットを管理し、開いたり閉じたりするために使用します。このエリアを使って、Single SnapshotsビューとCompare Snapshotsビューを切り替えることもできます。
Profile Analyzerと同様に、Memory Profilerでは2つのデータセット(メモリスナップショット)をロードして比較することができます。これは、時間経過やシーン間でメモリ使用量がどのように増加したかを調べたり、メモリリークを検索したりするときに特に便利です。
Memory Profilerのメインウィンドウには、Summary、Objects and Allocations、Fragmentationなど、メモリスナップショットを詳しく調べるためのタブが多数用意されている。それぞれのオプションについて詳しく見てみよう。
プロジェクトのメモリ使用量の概要をすばやく把握したいときに、このビューを選択します。また、キャプチャされたメモリースナップショットのメモリー関連の有用で重要な数値も含まれている。スナップショットが撮影された時点で何が起こっているのかを素早く見るには最適だ。
ツリーマップビューは、オブジェクトが使用するメモリの内訳をグラフィカルなツリーマップとして表示します。
ツリーマップビューの下には、選択されたグリッドセル内のオブジェクトのリストを表示するために更新されるフィルタリングされたテーブルがあります。
ツリーマップは、ネイティブまたはマネージドオブジェクトに帰属するメモリを示す。マネージド・オブジェクトのメモリーは、ネイティブ・オブジェクトのメモリーに矮小化される傾向があり、マップ・ビューで見つけるのが難しくなっている。ツリーマップを拡大してこれらを見ることもできるが、小さなオブジェクトを調べるには、通常、テーブルの方が概観がよくわかる。ツリーマップのセルをクリックすると、その下の表がそのセクションのタイプにフィルタリングされ、また、表の中の特定のオブジェクトが選択されます。
テーブルの行またはツリーマップのグリッドセルを選択し、[詳細]サイドパネルの[参照]セクションをチェックすることで、このリストでどのアイテムがオブジェクトを参照しているのか、そしておそらくこれらの参照がどのマネージドクラスのフィールドに存在するのかを追跡することができます。サイドが非表示になっている場合は、ツールバーの右上にあるトグルボタンで表示させることができる。
注:ツリーマップには、メモリ内のオブジェクトのみが表示されます。追跡された記憶の完全な表現ではない。これは、メモリ使用量の概要の数字が、追跡されたメモリの合計と同じでないことに気づいた場合に備えて理解しておくことが重要です。
これは、すべてのネイティブ・メモリーがオブジェクトと結びついているわけではないという事実に起因する。また、実行可能ファイルやDLL、NativeArraysなど、オブジェクトに関連しないNative Allocationsで構成されることもある。予約されているが未使用のメモリ領域」のような、より抽象的な概念も、ネイティブ・アロケーションの合計に含まれることがある。
オブジェクトと割り当て]ビューには、[すべてのオブジェクト]、[すべてのネイティブ・オブジェクト]、[すべての管理オブジェクト]、[すべてのネイティブ割り当て]などの既成の選択に基づいてフィルタリングするように切り替えることができるテーブルが表示されます。
選択した範囲のオブジェクト、アロケーション、またはメモリ・リージョンを表示するように、下の表を切り替えることができます。ツリーマップビューで述べたように、すべてのメモリがオブジェクトに関連付けられているわけではないので、All Memory RegionsとAll Native Allocationsのページでは、メモリ使用状況をより完全に把握することができます。
メモリ使用量を最適化し、メモリ予算が限られているハードウェアプラットフォームでより効率的にメモリをパックすることを目指す場合、これを活用する。
Memory Profilerスナップショットをロードし、ツリーマップ・ビューで、メモリ・フットプリント・サイズの大きいものから小さいものへと並べ、カテゴリーを検査する。
プロジェクト資産は多くの場合、メモリを最も消費する。テーブルビューを使って、Textureオブジェクト、Mesh、AudioClips、RenderTextures、Shaders、preallocated buffersを探します。これらはすべて、メモリの最適化に適した候補である。
メモリリークは通常、以下のような場合に起こる:
- オブジェクトは、コードを通じてメモリーから手動で解放されることはない。
- 意図的でない参照によってオブジェクトがメモリに残る
メモリー・プロファイラー比較モードは、特定の時間枠で2つのスナップショットを比較することで、メモリー・リークを見つけるのに役立ちます。
Unityのゲームでよくあるメモリリークのシナリオは、シーンのアンロード後に発生することがあります。
メモリ・プロファイラ・パッケージには、比較モードを使用してこれらのタイプのリークを発見するプロセスをガイドするワークフローがある。
複数のメモリ・スナップショットの差分比較を通じて、アプリケーションのライフタイム中に継続的にメモリが割り当てられる原因を特定することができます。
以下のセクションでは、プロジェクトで管理されているヒープ割り当てを特定するのに役立つヒントをいくつか挙げている。
Unity ProfilerのMemory Profilerモジュールは、フレームごとの管理された割り当てを赤い線で表します。これはほとんどの場合0であるべきなので、この行が急上昇した場合は、マネージド・アロケーションを調査すべきフレームであることを示している。
CPU使用率プロファイラー・モジュールのタイムライン・ビューでは、管理されたものを含むアロケーションがピンク色で表示される。
アロケーション・コール・スタックは、コード内のマネージド・メモリ割り当てを発見する素早い方法を提供する。これらは、ディープ・プロファイリングが通常追加するオーバーヘッドに比べ、少ないオーバーヘッドで必要なコールスタックの詳細を提供し、標準のプロファイラーを使ってその場で有効にすることができる。
プロファイラーでは、割り当てコールスタックはデフォルトで無効になっている。これらを有効にするには、ProfilerウィンドウのメインツールバーにあるCall Stacksボタンをクリックします。詳細ビューを関連データに変更する。
注:Unityの古いバージョン(Allocationコールスタックがサポートされる前)を使用している場合、ディーププロファイリングは、管理された割り当てを見つけるのに役立つ完全なコールスタックを取得するための良い方法です。
HierarchyまたはRaw Hierarchyで選択されたGC.Allocサンプルがコールスタックを含むようになりました。GC.Allocサンプルのコールスタックは、タイムラインの選択ツールチップでも確認できます。
CPU使用率プロファイラの階層ビューでは、カラムヘッダをクリックしてソート基準として使用することができます。GC Allocでソートするのは、それらに集中する素晴らしい方法だ。
Project Auditorは実験的な静的解析ツールである。このガイドの範囲外だが、プロジェクトを実行することなく、マネージド・アロケーションの原因となるプロジェクト内のコード1行1行のリストを作成することができる。この種の問題を見つけ、調査するには非常に効率的な方法だ。
UnityはBoehm-Demers-Weiserガベージコレクタを使用しており、プログラムコードの実行を停止し、作業が完了した時点で初めて通常の実行を再開します。
GCスパイクの原因となる不要なヒープ確保に注意。
- Strings:C#では、文字列は参照型であり、値型ではない。これは、たとえ一時的にしか使われないとしても、新しい文字列はすべて管理ヒープ上に割り当てられることを意味する。不必要な文字列の作成や操作を減らす。JSONやXMLのような文字列ベースのデータファイルの解析は避け、代わりにScriptableObjectやMessagePackやProtobufのような形式でデータを保存する。実行時に文字列を構築する必要がある場合は、StringBuilderクラスを使用する。
- ユニティの関数呼び出し:Unity API関数の中には、特に管理オブジェクトの配列を返すものなど、ヒープ割り当てを行うものがあります。ループの途中でアロケートするのではなく、配列への参照をキャッシュする。また、ゴミの発生を避ける特定の関数も活用しよう。例えば、手動で文字列とGameObject.tagを 比較する代わりに、GameObject.CompareTagを使用します(新しい文字列を返すとゴミが発生するため)。
- Boxing:参照型変数の代わりに値型変数を渡すことは避ける。これは一時的なオブジェクトを作成し、それに付随する潜在的なゴミは暗黙のうちに値の型をオブジェクト型に変換する(例えば、int i = 123; object o = i)。その代わりに、渡したい値型を具体的なオーバーライドとして提供するようにしよう。これらのオーバーライドにはジェネリックも使用できる。
- コルーチン:yieldはガベージを生成しないが、新しいWaitForSecondsオブジェクトの生成はガベージを生成する。WaitForSecondsオブジェクトは、yield行で作成したり、yield return nullを使用したりするのではなく、キャッシュして再利用する。
- LINQと正規表現:いずれも裏ボクシングからゴミが出る。パフォーマンスに問題がある場合は、LINQと正規表現は避けてください。forループを書き、新しい配列を作る代わりにリストを使う。
- ジェネリック・コレクションやその他の管理型:Updateで毎フレーム、Listやcollectionを宣言して入力するのはやめましょう(たとえば、プレイヤーの一定半径内にいる敵のリストなど)。代わりに、リストをMonoBehaviourのメンバーにして、Startで初期化します。コレクションを使用する前に、フレームごとにClearを使用してコレクションを空にするだけです。
可能な限りガベージコレクションを行う
ガベージコレクションのフリーズがゲーム内の特定のポイントに影響しないことが確実な場合は、System.GC.Collect.ガベージコレクションでガベージコレクションをトリガーすることができます。
自動メモリ管理の活用例については、「自動メモリ管理を理解する」を参照してください。
インクリメンタルガベージコレクタを使用してGC作業負荷を分割する
インクリメンタル・ガベージ・コレクションは、プログラムの実行中に1回の長い割り込みを発生させるのではなく、複数のフレームに作業負荷を分散させる複数の短い割り込みを使用する。ガベージコレクションが不規則なフレームレートを引き起こしている場合、このオプションを試してGCスパイクの問題を軽減できるかどうかを確認する。プロファイル・アナライザーを使用して、アプリケーションでの利点を確認してください。
インクリメンタルモードでGCを使用すると、いくつかのC#コールに読み書きバリアが追加され、スクリプトコールのオーバーヘッドが1フレームあたり最大~1ミリ秒追加される可能性があるオーバーヘッドが発生することに注意してください。最適なパフォーマンスを得るためには、メインゲームプレイのループにGC Allocsを持たないことが理想的です。そうすることで、スムーズなフレームレートのためにIncremental GCを必要とせず、GC.Collectをユーザーが気づかない場所、例えばメニューを開いたり、新しいレベルをロードしたりするときに非表示にすることができます。
メモリ・プロファイラの詳細については、以下のリソースをご覧ください:
- メモリ・プロファイラのドキュメント
- UnityチュートリアルのMemory Profilerでメモリ使用量を改善する
- メモリプロファイラーメモリ関連のトラブルシューティングツール Uniteセッション
- メモリプロファイラを使ったUnityラーニングセッション
電子書籍をダウンロードする Unityゲームプロファイリングアルティメットガイドを無料でダウンロードして、すべてのヒントとベストプラクティスを入手してください。