
ゲーム開発者のためのパフォーマンスプロファイリングのヒント
スムーズなパフォーマンスは、プレイヤーに没入感のあるゲーム体験を提供するために不可欠です。ゲームを最適化するためには、一貫したエンドツーエンドのプロファイリングワークフローが「必須」であり、効率的なゲーム開発のために、シンプルな三段階の手順から始まります:
- 大きな変更を加える前にプロファイルを取る:ベースラインを確立する。
- 開発中のプロファイリング:変更がパフォーマンスや予算を壊さないように追跡し、確認する。
- 後でプロファイルを取る:変更が望ましい効果を持っていたことを証明する。
このページでは、ゲーム開発者のための一般的なプロファイリングワークフローを概説しています。これは、Unityゲームのプロファイリングに関する究極のガイド 無料でダウンロード可能(ガイドのUnity 6バージョンは近日中に利用可能になります)から抜粋したものです。この電子書籍は、ゲーム開発、プロファイリング、最適化における外部および内部のUnity専門家によって作成されました。
この記事では、プロファイリングで設定するのに役立つ目標、CPUバウンドやGPUバウンドなどの一般的なパフォーマンスボトルネック、これらの状況をより詳細に特定し調査する方法について学ぶことができます。
フレーム予算を設定する
ゲーマーはパフォーマンスをフレームレート、つまり1秒あたりのフレーム数(fps)で測定することが多いですが、開発者としては一般的にフレーム時間(ミリ秒)を使用することが推奨されます。次の簡略化されたシナリオを考えてみてください:
ランタイム中に、あなたのゲームは0.75秒で59フレームをレンダリングします。しかし、次のフレームはレンダリングに0.25秒かかります。平均的に提供されるフレームレート60 fpsは良さそうですが、実際には最後のフレームがレンダリングに四分の一秒かかるため、プレイヤーはスタッター効果に気付くでしょう。
これが、フレームごとの特定の時間バジェットを目指すことが重要な理由の一つです。これにより、ゲームのプロファイリングと最適化に向けて取り組むための確固たる目標が提供され、最終的にはプレイヤーにとってよりスムーズで一貫した体験を生み出します。
各フレームには、ターゲットfpsに基づいた時間バジェットがあります。30 fpsをターゲットとするアプリケーションは、常に1フレームあたり33.33 ms未満(1000 ms / 30 fps)であるべきです。同様に、60 fpsのターゲットは1フレームあたり16.66 ms(1000 ms / 60 fps)を残します。
UIメニューの表示やシーンの読み込みなど、インタラクティブでないシーケンス中にこのバジェットを超えることはできますが、ゲームプレイ中にはできません。ターゲットフレームバジェットを超える単一のフレームでも、ヒッチを引き起こします。
注:VRゲームにおいて一貫して高いフレームレートを維持することは、プレイヤーに吐き気や不快感を引き起こさないために不可欠であり、プラットフォームホルダーからの認証を得るためにしばしば必要です。
1秒あたりのフレーム数:誤解を招く指標
ゲーマーがパフォーマンスを測定する一般的な方法は、フレームレート、つまり1秒あたりのフレーム数です。しかし、代わりにミリ秒単位のフレーム時間を使用することが推奨されます。なぜかを理解するには、fpsとフレーム時間の上のグラフを見てください。
これらの数字を考慮してください:
1000 ms/sec / 900 fps = 1.111 ms フレームあたり
1000 ms/sec / 450 fps = 2.222 ms フレームあたり
1000 ms/sec / 60 fps = 16.666 ms フレームあたり
1000 ms/sec / 56.25 fps = 17.777 ms フレームあたり
アプリケーションが900 fpsで動作している場合、これはフレームあたり1.111ミリ秒のフレーム時間に相当します。450 fpsでは、これはフレームあたり2.222ミリ秒です。これは、フレームレートが半分に見えるにもかかわらず、フレームあたりわずか1.111ミリ秒の違いを表しています。
60 fpsと56.25 fpsの違いを見ると、それぞれ16.666ミリ秒と17.777ミリ秒のフレーム時間に相当します。これもフレームあたり1.111ミリ秒の追加を表していますが、ここではフレームレートの低下は割合的にあまり劇的に感じません。
これが、開発者がfpsではなく平均フレーム時間を使用してゲームの速度をベンチマークする理由です。
ターゲットフレームレートを下回らない限り、fpsを心配しないでください。ゲームがどれだけ速く動作しているかを測定するためにフレーム時間に焦点を当て、その後フレーム予算内に留まってください。
詳細については、元の記事「ロバート・ダンロップのfps対フレーム時間」をお読みください。

モバイルの課題
サーマルコントロールは、モバイルデバイス向けのアプリケーションを開発する際に最適化する最も重要な領域の1つです。CPUまたはGPUが非効率的なコードのためにフルスロットルで長時間動作すると、これらのチップは熱くなります。過熱とチップへの潜在的な損傷を避けるために、オペレーティングシステムはデバイスのクロックスピードを下げて冷却を許可し、フレームのスタッタリングと悪いユーザー体験を引き起こします。このパフォーマンスの低下は、サーマルスロットリングとして知られています。
高いフレームレートとコード実行(またはDRAMアクセス操作)の増加は、バッテリーの消耗と熱の発生を引き起こします。パフォーマンスが悪いと、低スペックのモバイルデバイスの全セグメントでゲームがプレイできなくなり、市場機会を逃す可能性があります。
熱の問題に取り組む際は、システム全体の予算として作業するための予算を考慮してください。
ゲームを最初から最適化するために、早期にプロファイリングを行ってサーマルスロットリングとバッテリー消耗に対抗してください。サーマルとバッテリー消耗の問題に対抗するために、ターゲットプラットフォームのハードウェアに合わせてプロジェクト設定を調整してください。
モバイルのフレーム予算を調整する
デバイスの熱問題に対抗する一般的なヒントは、約35%のフレームアイドル時間を残すことです。これにより、モバイルチップが冷却され、過剰なバッテリー消耗を防ぐのに役立ちます。1フレームあたり33.33ms(30fps)のターゲットフレーム時間を使用すると、モバイルデバイスのフレーム予算は1フレームあたり約22msになります。
計算は次のようになります:(1000 ms / 30) * 0.65 = 21.66 ms
同じ計算を使用してモバイルで60fpsを達成するには、ターゲットフレーム時間が(1000ms / 60) * 0.65 = 10.83msが必要です。これは多くのモバイルデバイスで達成するのが難しく、30fpsをターゲットにするよりもバッテリーを2倍速く消耗させます。これらの理由から、多くのモバイルゲームは60fpsではなく30fpsをターゲットにしています。この設定を制御するにはApplication.targetFrameRateを使用し、フレーム時間に関する詳細はプロファイリングのe-bookの「フレーム予算を設定する」セクションを参照してください。
モバイルチップの周波数スケーリングは、プロファイリング時にフレームアイドル時間の予算配分を特定するのを難しくすることがあります。あなたの改善と最適化は純粋にプラスの効果を持つ可能性がありますが、モバイルデバイスは周波数を下げているかもしれず、その結果、冷却されている可能性があります。最適化の前後でモバイルチップの周波数、アイドル時間、スケーリングを監視するために、FTraceやPerfettoなどのカスタムツールを使用してください。
ターゲットfps(例えば30fpsの場合は33.33ms)の総フレーム時間予算内に留まり、デバイスがこのフレームレートを維持するために働いているか、温度が低く記録されているのを確認できれば、正しい方向に進んでいます。
モバイルデバイスのフレーム予算に余裕を持たせるもう一つの理由は、実際の温度変動を考慮するためです。暑い日には、モバイルデバイスが加熱し、熱を放散するのに問題が生じることがあり、これがサーマルスロットリングやゲームパフォーマンスの低下につながる可能性があります。このシナリオを避けるために、フレームバジェットのパーセントを確保してください。

メモリアクセス操作を減らす
DRAMアクセスは、モバイルデバイスでは通常、電力を多く消費する操作です。Armのモバイルデバイス上のグラフィックスコンテンツの最適化アドバイスによれば、LPDDR4メモリアクセスはバイトあたり約100ピコジュールのコストがかかります。
フレームごとのメモリアクセス操作の数を減らすには:
- フレームレートを下げる
- 可能な場合はディスプレイ解像度を下げる
- 頂点数と属性精度を減らしたシンプルなメッシュを使用する
- テクスチャ圧縮とミップマッピングを使用する
Arm CPUまたはGPUハードウェアを活用するデバイスに焦点を当てる必要がある場合、Arm Performance Studioツール(具体的には、Streamline Performance Analyzer)には、メモリ帯域幅の問題を特定するための優れたパフォーマンスカウンターが含まれています。利用可能なカウンターは、各Arm GPU世代に対して対応するユーザーガイドにリストされ、説明されています。例えば、Mali-G710パフォーマンスカウンターリファレンスガイドです。Arm Performance Studio GPUプロファイリングには、Arm ImmortalisまたはMali GPUが必要です。
ベンチマーク用のハードウェアティアを確立する
プラットフォーム固有のプロファイリングツールを使用することに加えて、サポートしたい各プラットフォームと品質のティアまたは最低スペックデバイスを確立し、これらの仕様ごとにパフォーマンスをプロファイリングおよび最適化します。
例えば、モバイルプラットフォームをターゲットにしている場合、ターゲットハードウェアに基づいて機能をオンまたはオフに切り替える品質コントロールを持つ3つのティアをサポートすることを決定するかもしれません。次に、各ティアの最低デバイス仕様に最適化します。別の例として、コンソール向けのゲームを開発している場合は、古いバージョンと新しいバージョンの両方でプロファイリングを行うことを確認してください。
私たちの最新のモバイル最適化ガイドには、サーマルスロットリングを減らし、ゲームを実行しているモバイルデバイスのバッテリー寿命を延ばすのに役立つ多くのヒントとトリックがあります。

高レベルから低レベルのプロファイリングへ
プロファイリングを行う際は、最大の影響を与えることができる領域に時間と労力を集中させることを確認したいです。したがって、プロファイリングを行う際には、レンダリング、スクリプト、物理学、ガベージコレクション(GC)割り当てなどのカテゴリの高レベルの概要から始めることをお勧めします。懸念のある領域を特定したら、より深い詳細に掘り下げることができます。この高レベルのパスを使用して、データを収集し、不要なマネージ割り当てやコアゲームループでの過剰なCPU使用を引き起こすシナリオを含む、最も重要なパフォーマンス問題についてメモを取ります。
まず、GC.Allocマーカーのコールスタックを収集する必要があります。このプロセスに不慣れな場合は、電子書籍の「アプリケーションの生存期間にわたる繰り返しメモリ割り当ての特定」というセクションでいくつかのヒントやコツを見つけてください。
報告されたコールスタックが割り当てのソースや他の遅延を追跡するのに十分な詳細でない場合は、割り当てのソースを見つけるために、Deep Profilingを有効にして2回目のプロファイリングセッションを実行できます。Deep Profilingについては電子書籍で詳しく説明していますが、要約すると、これはProfilerのモードで、すべての関数呼び出しの詳細なパフォーマンスデータをキャプチャし、実行時間や動作に関する詳細な洞察を提供しますが、標準的なプロファイリングに比べてかなり高いオーバーヘッドがあります。
フレーム時間の「違反者」についてメモを取る際は、他のフレームと比較してどのように比較されるかを必ず記録してください。この相対的な影響は、Deep Profilingが有効になっていると歪む可能性があります。なぜなら、Deep Profilingはすべてのメソッド呼び出しを計測することによってかなりのオーバーヘッドを追加するからです。
早期にプロファイルする
プロジェクトの開発サイクル全体を通じて常にプロファイリングを行うべきですが、プロファイリングから得られる最も重要な利益は、初期段階で始めるときに得られます。
早期に頻繁にプロファイリングを行い、あなたとあなたのチームがプロジェクトの「パフォーマンスシグネチャ」を理解し、記憶できるようにします。パフォーマンスが急落した場合、問題が発生したときに簡単に特定し、問題を修正できるようになります。
エディターでのプロファイリングは主要な問題を特定する簡単な方法を提供しますが、最も正確なプロファイリング結果は、ターゲットデバイスでビルドを実行しプロファイリングすることから得られ、各プラットフォームのハードウェア特性を掘り下げるためにプラットフォーム固有のツールを活用することが常に必要です。この組み合わせにより、すべてのターゲットデバイスにわたるアプリケーションパフォーマンスの全体像を提供します。例えば、あるモバイルデバイスではGPUに制約があるかもしれませんが、他のデバイスではCPUに制約があるかもしれません。これを学ぶには、これらのデバイスで測定する必要があります。
パフォーマンスの問題を特定する
このチャートの印刷可能なPDFバージョンをこちらからダウンロードしてください。
プロファイリングの目的は、最適化のターゲットとしてボトルネックを特定することです。推測に頼ると、ボトルネックでないゲームの部分を最適化してしまい、全体的なパフォーマンスにほとんど改善が見られない結果になる可能性があります。一部の「最適化」は、ゲームの全体的なパフォーマンスを悪化させる可能性があり、他のものは手間がかかるが、重要な結果をもたらさないことがあります。鍵は、集中した時間投資の影響を最適化することです。
上のフローチャートは、初期のプロファイリングプロセスを示しており、その後のセクションでは各ステップに関する詳細情報を提供しています。彼らはまた、実際のUnityプロジェクトからのプロファイラーキャプチャを提示し、注目すべき事柄の種類を示しています。
CPUのすべてのアクティビティの全体像を把握するために、GPUを待っているときも含めて、プロファイラーのタイムラインビューを使用してください。キャプチャを正しく解釈するために、一般的なプロファイラーマーカーに慣れてください。プロファイラーマーカーの一部は、ターゲットプラットフォームによって異なる場合があるため、各ターゲットプラットフォームでのゲームのキャプチャを探る時間を費やし、プロジェクトにとって「正常な」キャプチャがどのようなものかを把握してください。
プロジェクトのパフォーマンスは、最も時間がかかるチップおよび/またはスレッドによって制約されます。最適化の努力が集中すべき領域です。例えば、ターゲットフレーム時間のバジェットが33.33 msでVSyncが有効なゲームの次のシナリオを想像してください:
- CPUフレーム時間(VSyncを除く)が25 msでGPU時間が20 msの場合、問題ありません!あなたはCPUに制約されていますが、すべてがバジェット内であり、最適化してもフレームレートは改善されません(CPUとGPUの両方を16.66 ms未満にして60 fpsにジャンプしない限り)。
- CPUフレーム時間が40 msでGPUが20 msの場合、あなたはCPUに制約されており、CPUパフォーマンスを最適化する必要があります。GPUパフォーマンスを最適化しても役に立ちません。実際、適用可能な場合はC#コードの代わりにコンピュートシェーダーを使用することで、CPUの作業をGPUに移動させることを検討するかもしれません。
- CPUフレーム時間が20 msでGPUが40 msの場合、あなたはGPUに制約されており、GPUの作業を最適化する必要があります。
- CPUとGPUの両方が40 msの場合、両方に制約されており、30 fpsに到達するためには両方を33.33 ms未満に最適化する必要があります。
CPUまたはGPUに制約されていることをさらに探るこれらのリソースを参照してください:

フレームバジェットに収まっているか?
プロジェクトを早期に、かつ開発全体にわたってプロファイリングおよび最適化することで、アプリケーションのすべてのCPUスレッドと全体のGPUフレーム時間がフレームバジェット内に収まることを確実にするのに役立ちます。このプロセスを導く質問は、フレームバジェット内にいるかどうかですか?
上は、継続的なプロファイリングと最適化を行ったチームによって開発されたUnityモバイルゲームのプロファイルキャプチャの画像です。このゲームは、高スペックのモバイルフォンで60 fps、キャプチャにある中/低スペックのフォンで30 fpsを目指しています。
選択したフレームの時間のほぼ半分が、黄色のWaitForTargetFPSプロファイラーマーカーによって占められていることに注意してください。アプリケーションはApplication.targetFrameRateを30 fpsに設定しており、VSyncが有効になっています。メインスレッドでの実際の処理作業は約19 msのマークで終了し、次のフレームを開始する前に33.33 msの残りの時間が経過するのを待つのに使われます。この時間はプロファイラーマーカーで表されていますが、メインCPUスレッドはこの時間の間基本的にアイドル状態であり、CPUが冷却され、最小限のバッテリー電力を使用できるようにしています。
他のプラットフォームやVSyncが無効になっている場合、注意すべきマーカーは異なるかもしれません。重要なのは、メインスレッドがフレームバジェット内で実行されているか、アプリケーションがVSyncを待っていることを示す何らかのマーカーでフレームバジェットに正確にいるか、他のスレッドにアイドル時間があるかどうかを確認することです。
アイドル時間は、灰色または黄色のプロファイラーマーカーで表されます。上のスクリーンショットは、レンダースレッドがGfx.WaitForGfxCommandsFromMainThreadでアイドル状態であることを示しており、これは1フレームでGPUにドローコールを送信し終え、次のCPUからのドローコールリクエストを待っている時間を示します。同様に、Job Worker 0スレッドがCanvas.GeometryJobでいくつかの時間を費やしているものの、ほとんどの時間はアイドル状態です。これらはすべて、フレームバジェット内で快適に動作しているアプリケーションの兆候です。
ゲームがフレームバジェット内にある場合
フレームバジェット内にいる場合、バッテリー使用量やサーマルスロットリングを考慮してバジェットに加えた調整を含めて、主要なプロファイリングタスクは完了です。アプリケーションがメモリバジェット内にあることを確認するために、Memory Profilerを実行して結論を出すことができます。
上の画像は、30 fpsに必要な約22 msのフレームバジェット内で快適に動作しているゲームを示しています。VSyncまでのメインスレッド時間をパディングするWaitForTargetfpsと、レンダースレッドおよびワーカースレッドの灰色のアイドル時間に注意してください。また、Gfx.Presentフレームの終了時間を見てVBlank間隔を観察でき、これらの間の時間を測定するためにTimelineエリアまたは上部のTimeルーラーに時間スケールを描くことができます。

CPUバウンド
ゲームがCPUフレームバジェット内にない場合、次のステップはCPUのどの部分がボトルネックになっているかを調査することです。つまり、どのスレッドが最も忙しいかです。
全体のCPU負荷がボトルネックになることは稀です。現代のCPUは、独立して同時に作業を行うことができる異なるコアを多数持っています。異なるスレッドは各CPUコアで実行できます。完全なUnityアプリケーションは、さまざまな目的のために一連のスレッドを使用しますが、パフォーマンスの問題を見つけるために最も一般的なものは次のとおりです:
- メインスレッド:ここでは、ゲームロジック/スクリプトの大部分がデフォルトで作業を行います。物理、アニメーション、UI、レンダリングの初期段階など、ほとんどのUnityシステムはここで実行されます。
- レンダースレッド:これは、GPUにレンダリング指示を送信する前に発生しなければならない準備作業(例:シーン内のどのオブジェクトがカメラに見えるか、視錐台の外にあるために除外/非表示にされているオブジェクト、他の基準によって隠されているオブジェクト)を処理します。
- レンダリングプロセス中、メインスレッドはシーンを調べ、カメラのカリング、深度ソート、ドローコールのバッチ処理を行い、レンダリングするもののリストを生成します。このリストはレンダースレッドに渡され、Unityの内部プラットフォーム非依存の表現から、特定のプラットフォームでGPUに指示を出すために必要な特定のグラフィックスAPI呼び出しに変換されます。
- ジョブワーカースレッド:開発者は、メインスレッドの負荷を軽減するために、ワーカースレッドで実行される特定の種類の作業をスケジュールするためにジョブシステムを利用できます。Unityのシステムや機能のいくつかも、物理、アニメーション、レンダリングなどのジョブシステムを利用しています。
メインスレッド最適化の実世界の例
下の画像は、メインスレッドに制約のあるプロジェクトがどのように見えるかを示しています。このプロジェクトはMeta Quest 2で実行されており、通常は13.88 ms(72 fps)または8.33 ms(120 fps)のフレームバジェットをターゲットにしています。高いフレームレートはVRデバイスでの動きの病気を避けるために重要です。しかし、このゲームが30 fpsをターゲットにしていたとしても、このプロジェクトが問題に直面していることは明らかです。
レンダースレッドとワーカースレッドは、フレームバジェット内の例に似ているように見えますが、メインスレッドはフレーム全体にわたって明らかに作業に忙しいです。フレームの最後に小さなプロファイラーオーバーヘッドを考慮しても、メインスレッドは45ミリ秒以上忙しく、つまりこのプロジェクトは22 fps未満のフレームレートを達成しています。メインスレッドがVSyncを待っていることを示すマーカーはなく、フレーム全体にわたって忙しいです。
調査の次の段階は、最も時間がかかるフレームの部分を特定し、なぜそうなるのかを理解することです。このフレームでは、PostLateUpdate.FinishFrameRenderingが16.23ミリ秒かかり、フレームバジェット全体を超えています。詳細な検査により、Inl_RenderCameraStackというマーカーが5つ存在し、5つのカメラがアクティブでシーンをレンダリングしていることが示されています。Unityのすべてのカメラがカリング、ソート、バッチ処理を含む全レンダーパイプラインを呼び出すため、このプロジェクトの最優先タスクはアクティブカメラの数を減らすことであり、理想的には1つだけにすることです。
BehaviourUpdate、すべてのMonoBehaviour.Update()メソッドの実行を含むプロファイラーマーカーは、このフレームで7.27ミリ秒かかります。
タイムラインビューでは、マゼンタ色のセクションがスクリプトがマネージヒープメモリを割り当てているポイントを示します。Hierarchyビューに切り替え、検索バーにGC.Allocと入力してフィルタリングすると、このメモリの割り当てに約0.33ミリ秒かかることが示されます。しかし、それはメモリ割り当てがCPUパフォーマンスに与える影響の不正確な測定です。
GC.Allocマーカーは、典型的なプロファイラーサンプルのように開始点と終了点を記録することで時間を計測されません。オーバーヘッドを最小限に抑えるために、Unityは割り当てのタイムスタンプと割り当てられたサイズのみを記録します。
プロファイラーは、GC.Allocマーカーに小さな人工的なサンプル継続時間を割り当てて、プロファイラービューで可視化されるようにします。実際の割り当ては、特に新しいメモリ範囲をシステムから要求する必要がある場合、より長くかかることがあります。影響をより明確に見るために、割り当てを行うコードの周りにプロファイラーマーカーを配置します。深いプロファイリングでは、タイムラインビューのマゼンタ色のGC.Allocサンプル間の隙間が、どれくらいの時間がかかったかの指標を提供します。
さらに、新しいメモリの割り当ては、測定が難しく、直接的に帰属させることができないパフォーマンスに悪影響を及ぼす可能性があります。
- システムから新しいメモリを要求すると、モバイルデバイスの電力バジェットに影響を与え、システムがCPUまたはGPUの速度を遅くする可能性があります。
- 新しいメモリは、CPUのL1キャッシュにロードされる必要があり、既存のキャッシュラインを押し出すことになります。
- インクリメンタルまたは同期的なガベージコレクションは、マネージメモリ内の既存の空きスペースが最終的に超過されると、直接または遅延でトリガーされる可能性があります。
フレームの開始時に、4つのPhysics.FixedUpdateのインスタンスが合計で4.57ミリ秒になります。後で、LateBehaviourUpdate(MonoBehaviour.LateUpdate()への呼び出し)は4msかかり、Animatorsは約1msを占めます。このプロジェクトが望ましいフレーム予算とレートに達するためには、これらのメインスレッドの問題をすべて調査して、適切な最適化を見つける必要があります。
メインスレッドのボトルネックに関する一般的な落とし穴
最も時間がかかるものを最適化することで、最大のパフォーマンス向上が得られます。以下の領域は、メインスレッドに依存するプロジェクトで最適化を探すのにしばしば有益な場所です:
- 物理計算
- MonoBehaviourスクリプトの更新
- ガーベジ割り当ておよび/またはコレクション
- メインスレッドでのカメラのカリングとレンダリング
- 非効率的なドローコールのバッチ処理
- UIの更新、レイアウト、および再構築
- アニメーション
最も一般的な落とし穴を最適化するための実行可能なヒントの長いリストを提供する最適化ガイドをお読みください:
調査したい問題に応じて、他のツールも役立つ場合があります:
- 長時間かかるが、なぜそうなるのか正確に示さないMonoBehaviourスクリプトには、コードにProfiler Markersを追加するか、深いプロファイリングを試して、完全なコールスタックを確認してください。
- マネージメモリを割り当てるスクリプトの場合、Allocation Call Stacksを有効にして、割り当てがどこから来ているのか正確に確認してください。または、深いプロファイリングを有効にするか、Project Auditorを使用して、メモリでフィルタリングされたコードの問題を表示し、マネージ割り当てを引き起こすすべてのコード行を特定できます。
- Frame Debuggerを使用して、ドローコールのバッチ処理が不良な原因を調査します。
ゲームの最適化に関する包括的なヒントについては、これらのUnity専門家ガイドを無料でダウンロードしてください:

CPUバウンド:レンダースレッド
こちらは、レンダースレッドにバウンドされた実際のプロジェクトです。これは、等角視点を持ち、ターゲットフレームバジェットが33.33 msのコンソールゲームです。
プロファイラーのキャプチャは、現在のフレームのレンダリングを開始する前に、メインスレッドがレンダースレッドを待機していることを示しています。これは、Gfx.WaitForPresentOnGfxThread マーカーによって示されています。レンダースレッドは、前のフレームからのドローコールコマンドをまだ送信しており、メインスレッドからの新しいドローコールを受け入れる準備ができていません。また、Camera.Renderで時間を費やしています。
現在のフレームに関連するマーカーと他のフレームからのマーカーの違いを見分けることができます。後者はより暗く表示されます。メインスレッドが続行できるようになり、レンダースレッドが処理するためのドローコールを発行し始めると、レンダースレッドは現在のフレームを処理するのに100 ms以上かかり、次のフレーム中にボトルネックを生じます。
さらなる調査により、このゲームは複雑なレンダリングセットアップを持ち、9つの異なるカメラと置き換えシェーダーによって引き起こされる多くの追加パスが含まれていることがわかりました。このゲームは、フォワードレンダリングパスを使用して130以上のポイントライトをレンダリングしており、各ライトに対して複数の追加の透明ドローコールを追加する可能性があります。合計で、これらの問題が組み合わさって、フレームごとに3000以上のドローコールを生成しました。
レンダースレッドのボトルネックに関する一般的な落とし穴
以下は、レンダースレッドバウンドのプロジェクトで調査すべき一般的な原因です:
- ドローコールのバッチ処理が不十分:これは、OpenGLやDirectX 11などの古いグラフィックスAPIに特に当てはまります。
- カメラが多すぎる:スプリットスクリーンのマルチプレイヤーゲームを作成していない限り、アクティブなカメラは1つだけであるべきです。
- カリングが不十分:これにより、描画されるものが多すぎます。カメラのフラスタムの寸法とカリングレイヤーマスクを調査してください。
レンダリングプロファイラモジュールは、毎フレームのドローコールバッチとSetPassコールの数の概要を示します。GPUに送信される描画コールのバッチを調査するための最良のツールはフレームデバッガーです。
ボトルネックを解決するためのツール
この電子書籍の焦点はパフォーマンスの問題を特定することですが、以前に強調した2つの補完的なパフォーマンス最適化ガイドは、ターゲットプラットフォームがPCまたはコンソールかモバイルかに応じてボトルネックを解決する方法に関する提案を提供します。レンダースレッドのボトルネックの文脈では、Unityが特定した問題に応じて異なるバッチ処理システムとオプションを提供していることを強調する価値があります。以下は、電子書籍で詳細に説明しているいくつかのオプションの簡単な概要です。
- SRPバッチ処理は、マテリアルデータをGPUメモリに永続的に保存することでCPUのオーバーヘッドを削減します。実際の描画コールの数を減らすわけではありませんが、各描画コールを安価にします。
- GPUインスタンシングは、同じマテリアルを使用する同じメッシュの複数のインスタンスを1つの描画コールにまとめます。
- 静的バッチ処理は、同じマテリアルを共有する静的(動かない)メッシュを結合し、多くの静的要素を持つレベルデザインでの利点を提供します。
- GPU常駐ドロワーは、類似のGameObjectをグループ化することでCPUのオーバーヘッドと描画コールを削減するために自動的にGPUインスタンシングを使用します。
- 動的バッチ処理は、ランタイムで小さなメッシュを結合し、高い描画コストを持つ古いモバイルデバイスでの利点となる可能性があります。ただし、欠点は頂点変換がリソース集約的である可能性があることです。
- GPUオクルージョンカリングは、現在のフレームと前のフレームの深度バッファを比較することでオブジェクトの可視性を判断するためにコンピュートシェーダーを使用し、事前に焼き付けたデータを必要とせずにオクルードされたオブジェクトの不要なレンダリングを削減します。
さらに、CPU側では、Camera.layerCullDistancesのような技術を使用して、カメラからの距離に基づいてオブジェクトをカリングすることでレンダースレッドに送信されるオブジェクトの数を減らし、カメラカリング中のCPUボトルネックを緩和するのに役立ちます。
これらは利用可能なオプションの一部に過ぎません。これらのそれぞれには異なる利点と欠点があります。いくつかは特定のプラットフォームに制限されています。プロジェクトは、これらのシステムのいくつかを組み合わせて使用する必要があり、そのためにはそれらを最大限に活用する方法を理解する必要があります。

CPUバウンド:ワーカースレッド
メインスレッドやレンダースレッド以外のCPUスレッドに制約されるプロジェクトはそれほど一般的ではありません。ただし、プロジェクトがデータ指向技術スタック(DOTS)を使用し、作業がメインスレッドからワーカースレッドに移動される場合、特にジョブシステムを使用している場合には発生する可能性があります。
上記の画像は、エディターの再生モードでのキャプチャで、CPU上でパーティクル流体シミュレーションを実行しているDOTSプロジェクトを示しています。
一見すると成功のように見えます。ワーカースレッドはBurst-compiledジョブでぎっしり詰まっており、メインスレッドから多くの作業が移動したことを示しています。通常、これは良い決定です。
しかし、この場合、48.14 msのフレーム時間とメインスレッドの35.57 msの灰色のWaitForJobGroupIDマーカーは、すべてがうまくいっていない兆候です。WaitForJobGroupIDは、メインスレッドがワーカースレッドで非同期に実行するジョブをスケジュールしていることを示していますが、ワーカースレッドがそれらのジョブを実行し終える前に、その結果が必要です。WaitForJobGroupIDの下にある青いプロファイラーマーカーは、メインスレッドが待機中にジョブを実行していることを示しており、ジョブが早く終了するように努めています。
ジョブはBurst-compiledですが、依然として多くの作業を行っています。おそらく、このプロジェクトで使用されている空間クエリ構造は、近くにあるパーティクルを迅速に見つけるために最適化するか、より効率的な構造に置き換える必要があります。または、空間クエリジョブをフレームの開始ではなく終了にスケジュールし、結果は次のフレームの開始まで必要ないようにすることができます。おそらく、このプロジェクトはあまりにも多くのパーティクルをシミュレートしようとしています。ジョブのコードをさらに分析する必要があり、より細かいプロファイラーマーカーを追加することで、最も遅い部分を特定するのに役立ちます。
あなたのプロジェクトのジョブは、この例のように並列化されていないかもしれません。おそらく、単一のワーカースレッドで実行されている長いジョブが1つだけあるのかもしれません。これは問題ありませんが、ジョブがスケジュールされてから完了するまでの時間が、ジョブが実行されるのに十分長い必要があります。そうでない場合、上記のスクリーンショットのように、ジョブが完了するのを待っている間にメインスレッドがストールするのが見えるでしょう。
ワーカースレッドのボトルネックに関する一般的な落とし穴
同期ポイントとワーカースレッドのボトルネックの一般的な原因には、以下が含まれます:
- ジョブがBurstコンパイラーによってコンパイルされていない
- 単一のワーカースレッドでの長時間実行されるジョブが、複数のワーカースレッドに並列化されていない
- ジョブがスケジュールされるフレーム内のポイントと、結果が必要なポイントの間の時間が不十分です。
- すべてのジョブが即座に完了する必要があるフレーム内の複数の「同期ポイント」。
CPU使用率プロファイラモジュールのタイムラインビューで、ジョブがスケジュールされるタイミングと、メインスレッドによって結果が期待されるタイミングを調査するには、フローイベント機能を使用できます。
効率的なDOTSコードの記述に関する詳細は、DOTSベストプラクティスガイドを参照してください。

GPUバウンド
メインスレッドがGfx.WaitForPresentOnGfxThreadのようなプロファイラマーカーで多くの時間を費やし、レンダースレッドが同時にGfx.PresentFrameや.WaitForLastPresent.のようなマーカーを表示している場合、アプリケーションはGPUバウンドです。
GPUフレーム時間を取得する最良の方法は、ターゲットプラットフォーム固有のGPUプロファイリングツールを使用することですが、すべてのデバイスが信頼できるデータをキャプチャするのを容易にするわけではありません。
その場合、FrameTimingManager APIが役立ち、CPUとGPUの両方で低オーバーヘッドの高レベルのフレーム時間を提供します。
上記のキャプチャは、VulkanグラフィックスAPIを使用してAndroidモバイルフォンで取得されました。この例のGfx.PresentFrameで費やされた時間の一部はVSyncを待つことに関連しているかもしれませんが、このプロファイラマーカーの極端な長さは、この時間の大部分がGPUが前のフレームのレンダリングを完了するのを待つことに費やされていることを示しています。
このゲームでは、特定のゲームプレイイベントが、GPUによってレンダリングされる描画呼び出しの数を3倍にするシェーダーの使用を引き起こしました。GPUパフォーマンスをプロファイリングする際に調査すべき一般的な問題には、以下が含まれます:
- アンビエントオクルージョンやブルームのような高価なフルスクリーンポストプロセスエフェクト。
- 以下によって引き起こされる高価なフラグメントシェーダー:
- シェーダーコード内の分岐ロジック。
- 特にモバイルで、半精度ではなくフルフロート精度を使用すること。
- レジスタの過剰使用は、GPUのウェーブフロントの占有率に影響を与えます。
- 以下によって引き起こされる透明レンダーキューでのオーバードロー:
- 非効率的なUIレンダリング。
- パーティクルシステムの重複または過剰使用。
- ポストプロセッシングエフェクト
- 過度に高い画面解像度、例えば:
- 4Kディスプレイ
- モバイルデバイスのRetinaディスプレイ
- マイクロ三角形は以下によって引き起こされます:
- 密なメッシュジオメトリ
- レベルオブディテール(LOD)システムの欠如、これはモバイルGPUに特に問題ですが、PCやコンソールGPUにも影響を与える可能性があります
- キャッシュミスと無駄なGPUメモリ帯域幅は以下によって引き起こされます:
- 非圧縮テクスチャ
- ミップマップなしの高解像度テクスチャ
- ジオメトリまたはテッセレーションシェーダー、動的シャドウが有効になっている場合、フレームごとに複数回実行される可能性があります
アプリケーションがGPUに制約されているように見える場合、フレームデバッガーを使用してGPUに送信されるドローコールバッチを理解するための迅速な方法として利用できます。ただし、このツールは特定のGPUタイミング情報を表示することはできず、全体のシーンがどのように構成されているかのみを示します。
GPUボトルネックの原因を調査する最良の方法は、適切なGPUプロファイラーからのGPUキャプチャを調べることです。使用するツールは、ターゲットハードウェアと選択したグラフィックスAPIによって異なります。詳細については、電子書籍のプロファイリングとデバッグツールセクションを参照してください。


Unityのベストプラクティスハブからのさらなるベストプラクティスとヒントを見つけてください。業界の専門家やUnityのエンジニア、テクニカルアーティストが作成した30以上のガイドから選択し、Unityのツールセットとシステムを効率的に開発するのに役立ててください。