Hero background image

ゲーム開発者向けのパフォーマンスプロファイリングのヒント

このウェブページは、お客様の便宜のために機械翻訳されたものです。翻訳されたコンテンツの正確性や信頼性は保証いたしかねます。翻訳されたコンテンツの正確性について疑問をお持ちの場合は、ウェブページの公式な英語版をご覧ください。

プレイヤーに没入感のあるゲーム体験を提供するには、スムーズなパフォーマンスが不可欠です。ゲームを確実に最適化するには、一貫したエンドツーエンドのプロファイリング ワークフローがゲーム開発の効率化に不可欠です。このワークフローは、次の3つのシンプルな手順から始まります。

  • 大きな変更を加える前のプロファイル:ベースラインを確立する。
  • 開発時のプロファイリング:変更を追跡し、パフォーマンスや予算を超過しないようにします。
  • 次の時間経過後にプロファイルを作成:変更が期待した効果をもたらしたことを証明します。

このページでは、ゲーム開発者向けの一般的なプロファイリングワークフローの概要を説明します。このガイドは、無料でダウンロードできるeBook「Ultimate guide to profiling Unity games」 から抜粋したものです(Unity 6バージョンは近日公開予定です)。このeブックは、ゲーム開発、プロファイリング、最適化のUnity外部と内部の両方のエキスパートによって作成されました。

この記事では、プロファイリングで設定すべき有用な目標、CPU バウンドや GPU バウンドなど、一般的なパフォーマンスのボトルネック、およびこれらの状況の特定と調査の方法について詳しく説明します。

FPS フレームレートのグラフ

フレームバジェットの設定

ゲーマーは多くの場合、フレームレート、つまりFPS(フレーム/秒)を使用してパフォーマンスを測定しますが、開発者は通常、フレーム時間単位での使用を推奨します。以下の簡略化されたシナリオを考えてみましょう。

ランタイム中、ゲームは 0.75 秒で 59 フレームをレンダリングします。ただし、次のフレームのレンダリングには 0.25 秒かかります。平均 60 FPS のフレームレートは良好に聞こえますが、実際には、最後のフレームのレンダリングには 1/4 秒かかるため、スタッター効果に気付くでしょう。

これは、フレームごとに特定のタイムバジェットを目指すことが重要である理由の 1 つです。これにより、ゲームをプロファイリングおよび最適化する際に目指すべき確固たる目標が達成され、最終的には、よりスムーズで一貫性のある体験をプレイヤーに提供できます。

各フレームには、目標 FPS に基づくタイムバジェットがあります。30 FPS を目標とするアプリケーションでは、常に 1 フレームあたり 33.33 ミリ秒未満で済むようにする必要があります(1000 ミリ秒 / 30 FPS)。同様に、60 FPS の目標は、1 フレームあたり 16.66 ミリ秒のままです (1000 ミリ秒 / 60 FPS)。

このバジェットは、例えば UI メニューやシーンのロードなど、インタラクティブでない Sequences の間は超えてもかまいませんが、ゲームプレイ中は超えられません。1 フレームでもターゲットフレームバジェットを超えると、ヒッチが発生します。

注:VRゲームで一貫して高いフレームレートを維持することは、プレイヤーに不快感や不快感を与えないために不可欠であり、多くの場合、ゲームがプラットフォームホルダーの認定を受けるために必要です。

1 秒あたりのフレーム数:誤解を招く指標

ゲーマーがパフォーマンスを測定する一般的な方法は、フレームレート(1 秒あたりのフレーム数)です。ただし、代わりにミリ秒単位のフレーム時間を使用することをお勧めします。その理由を理解するには、上の FPS 対フレーム時間のグラフをご覧ください。

以下の数字を考慮してください。

1000 ms/秒/900 FPS = 1.111 ms/フレーム

1000 ms/秒/450 FPS = 2.222 ms/フレーム

1000 ms/秒/60 FPS = 16.666 ms/フレーム

1000 ms/秒 / 56.25 FPS = 17.777 ms/フレーム

アプリケーションが 900 FPS で実行されている場合、これは 1 フレームあたり 1.111 ミリ秒のフレーム時間に変換されます。450 FPS の場合、これは 1 フレームあたり 2.222 ミリ秒です。これは、フレームレートが半分に低下しているように見えても、1 フレームあたりわずか 1.111 ミリ秒の差です。

60 FPSと56.25 FPSの違いを見ると、それぞれ1フレームあたり16.666ミリ秒と17.777ミリ秒になります。これも 1 フレームあたり 1.111 ミリ秒余分に発生しますが、割合で見れば、フレームレートの低下ははるかに少ないように見えます。

そのため、開発者はゲームスピードのベンチマークに FPS ではなく平均フレーム時間を使用します。

FPS は、目標フレームレートを下回らない限り気にする必要はありません。フレーム時間を重視してゲームの実行速度を測定し、フレームバジェット内に収めましょう。

詳細については、元の記事「Robert DunlopのFPS対フレーム時間」をお読みください。

モバイルの課題

モバイルの課題

熱制御は、モバイルデバイス向けのアプリケーションを開発する際に最適化すべき最も重要な領域の1つです。非効率的なコードが原因でCPUやGPUがフル スロットルでの作業に長時間費やすと、これらのチップは高温になります。過熱やチップの損傷を避けるため、オペレーティングシステムはデバイスのクロックスピードを下げて冷却し、フレームスタッターやユーザー体験の低下を引き起こします。このパフォーマンス削減はサーマルスロットリングと呼ばれます。

フレームレートが向上し、コード実行(または DRAM アクセス操作)が増えると、バッテリーの消耗と発熱が増加します。また、パフォーマンスが悪いと、ローエンドのモバイルデバイスの全セグメントでゲームがプレイできなくなり、市場機会を失う可能性があります。

熱の問題に取り組む場合は、システム全体のバジェットェットとして考えてください。

サーマルスロットリングやバッテリーの消耗には、早期にプロファイリングを行って対処し、ゲームの開始から最適化しましょう。ターゲットプラットフォームハードウェアのプロジェクト設定を調整して、熱とバッテリーの消耗の問題に対処します。

モバイルでのフレーム予算の調整

プレイ時間が長くなることによるデバイス温度の問題に対処するための一般的なヒントは、フレームのアイドル時間を約35%にすることです。これにより、モバイルチップを冷却する時間が確保され、バッテリーの過剰な消耗を防ぐことができます。目標フレーム時間を 1 フレームあたり 33.33 ミリ秒(30 FPS の場合)とすると、モバイルデバイスのフレームバジェットは 1 フレームあたり約 22 ミリ秒になります。

計算は以下のようになります。(1000 ms / 30) * 0.65 = 21.66 ms

モバイルで同じ計算を使用して60 FPSを達成するには、目標フレーム時間(1000ミリ秒/60) * 0.65 = 10.83ミリ秒が必要です。これは多くのモバイルデバイスで実現が難しく、目標の30 FPSの2倍の速さでバッテリーを消費します。これらの理由から、多くのモバイルゲームは 60 FPS ではなく 30 FPS を目標としています。この設定は Application.targetFrameRate を使用して制御します。フレーム時間の詳細については、プロファイリング e ブックの「フレームバジェットの設定」セクションを参照してください。

モバイルチップでの頻度スケーリングにより、プロファイリング時にフレームのアイドル時間バジェット割り当てを特定するのが難しくなる場合があります。改善や最適化は純粋にプラスの効果をもたらしますが、モバイルデバイスのスケーリング頻度が下がり、その結果、動作温度が下がる可能性があります。FTracePerfettoなどのカスタムツールを使用して、モバイルチップの周波数、アイドル時間、最適化前後のスケーリングを監視します。

目標のFPSの合計フレーム時間バジェット(30 FPSの場合は33.33ミリ秒)内に収め、デバイスの動作を少なくしたり、このフレームレートを維持するために温度を低く抑えたりすることができれば、正しい方向に進んでいます。

モバイルデバイスのフレームバジェットに余裕を持たせるもう 1 つの理由は、現実世界の温度変動をアカウントすることです。暑い日には、モバイルデバイスが発熱して放熱が難しくなり、サーマルスロットリングやゲームパフォーマンスの低下につながる可能性があります。このシナリオを回避するヘルプとして、フレーム バジェットのパーセントを設定します。

メモリアクセス操作の削減

メモリアクセス操作の削減

DRAM アクセスは通常、モバイル デバイスで電力を消費する操作です。ARMのモバイルデバイス向けグラフィックスコンテンツ最適化アドバイスによると、LPDDR4メモリアクセスのコストは1バイトあたり約100ピコジュールです。

フレームあたりのメモリアクセス操作の数を減らす:

  • フレームレートの低下
  • 可能な限りディスプレイ解像度を下げる
  • 頂点数と属性精度を抑えたシンプルなメッシュの使用
  • テクスチャ圧縮とミップマッピングの使用

ARM CPUまたはGPUハードウェアを利用するデバイスに焦点を当てる必要がある場合、ARM Performance Studioツール(具体的にはStreamline Performance Analyzer)には、メモリ帯域幅の問題を特定するための優れたパフォーマンスカウンターがいくつか含まれています。ARM GPUの各世代で使用可能なカウンターが一覧され、対応するユーザーガイド(例えば、Mali-G710 Performance Counter Reference Guide)で説明されています。ARM Performance Studio GPUプロファイリングには、ARM ImmortalisまたはMali GPUが必要です。

ベンチマークのためのハードウェア階層の確立

プラットフォーム固有のプロファイリングツールの使用に加えて、サポートするプラットフォームと品質レベルごとに階層または最低スペックのデバイスを設定し、これらの仕様ごとにパフォーマンスをプロファイリングして最適化します。

たとえば、モバイルプラットフォームをターゲットにしている場合、ターゲットハードウェアに基づいて機能のオン/オフをトグル品質管理を備えた 3 つのティアをサポートすることを決めることができます。次に、各階層で最も低いデバイス仕様に合わせて最適化します。もう 1 つの例として、コンソール向けのゲームを開発している場合は、古いバージョンと新しいバージョンの両方でプロファイリングを行うようにしてください。

最新のモバイル最適化ガイドには、ゲームを実行するモバイルデバイスのサーマルスロットリングを減らし、バッテリー寿命を延ばすのに役立つヒントやコツが多数用意されています。

高度なプロファイリングから低レベルのプロファイリングまで

プロファイリングでは、最も大きな影響を与える可能性のある領域に時間と労力を集中させる必要があります。したがって、プロファイリング時には上から下へのアプローチから開始することをお勧めします。つまり、レンダリング、スクリプト、物理演算、GC(ガベージコレクション)割り当てなどのカテゴリの概要から開始します。関心領域を特定したら、さらに深く掘り下げることができます。この概要パスを使用して、コアゲームループで不要なマネージ割り当てや過剰なCPU使用率を引き起こすシナリオなど、パフォーマンスに関する最も重要な問題に関するデータを収集し、メモを取ることができます。

まず、GC.Allocマーカー用のコールスタックを収集する必要があります。このプロセスに馴染みのない方は、eBookの「アプリケーション生存期間にわたって繰り返し発生するメモリ割り当てを特定する」セクションでヒントやコツをご確認ください。

報告されたコールスタックの詳細が十分でなく、アロケーションやその他の速度低下のソースを突き止められない場合は、ディーププロファイリングを有効にして 2 回目のプロファイリングセッションを実行し、アロケーションのソースを見つけることができます。詳細なプロファイリングについてはeブックで詳しく説明していますが、要約すると、すべての関数呼び出しの詳細なパフォーマンス データを収集するプロファイラーのモードであり、実行時間と動作に関する詳細なインサイトを提供しますが、標準のプロファイリングに比べてオーバーヘッドが大幅に高くなります。

フレーム時間の「違反者」に関するメモを収集する場合は、フレームの他の部分との比較に注意してください。ディーププロファイリングを有効にすると、すべてのメソッド呼び出しが計測されて大きなオーバーヘッドが追加されるため、この相対的な影響が歪むことがあります。

早期にプロファイリング

プロジェクトの開発サイクル全体を通して常にプロファイリングを行う必要がありますが、プロファイリングから得られる最も大きな利点は、初期段階での開始です。

早い段階で頻繁にプロファイリングを行い、あなたとあなたのチームがベンチマークに使用できるプロジェクトの「パフォーマンスシグネチャ」を理解して暗記できるようにします。パフォーマンスが大幅に低下した場合、問題の発生場所を簡単に特定して問題を修正できます。

エディターでのプロファイリングにより、主な問題を簡単に特定できますが、最も正確なプロファイリング結果は、ターゲットデバイスでビルドを実行およびプロファイリングし、プラットフォーム固有のツールを活用して各プラットフォームのハードウェア特性を掘り下げたものです。この組み合わせにより、すべてのターゲット デバイスにわたるアプリケーション パフォーマンスの総合的なビューが提供されます。例えば、あるモバイルデバイスではGPUバウンドで、別のモバイルデバイスではCPUバウンドになっているとします。これは、それらのデバイスで測定しないとわかりません。

プロファイリングのフローチャート

パフォーマンスの問題を特定する

このグラフの印刷用 PDF バージョンは、こちらからダウンロードできます。

プロファイリングのポイントは、ボトルネックを最適化のターゲットとして特定することです。当て推量に頼ると、ボトルネックになっていない部分を最適化することになり、全体的なパフォーマンスはほとんど、またはまったく向上しません。「最適化」の中には、ゲーム全体のパフォーマンスを悪化させるものもある一方で、労力はかかるものの取るに足らない結果をもたらすものもあるかもしれません。キーは、集中した時間投資の効果を最適化することです。

上のフローチャートは、最初のプロファイリングプロセスを示しており、各ステップの詳細情報については、後続のセクションを参照してください。また、実際の Unity プロジェクトからのプロファイラーのキャプチャも紹介し、確認すべき項目の種類を説明します。

GPUの待機時間を含め、すべてのCPUアクティビティの全体像を把握するには、プロファイラーのCPUモジュールTimelineビューを使用します。キャプチャを正しく解釈するために、共通プロファイラーマーカーに慣れる。プロファイラーマーカーの一部は、ターゲットプラットフォームによって表示が異なる場合があります。各ターゲットプラットフォームでのゲームのキャプチャを調べて、プロジェクトでの「通常の」キャプチャがどのようなものかを感じ取ってください。

プロジェクトのパフォーマンスは、最も時間がかかるチップやスレッドによって左右されます。最適化の取り組みに注力すべき分野です。例えば、ターゲットフレーム時間バジェットが 33.33 ミリ秒で、VSync が有効になっているゲームについて、以下のシナリオがあるとします。

  • CPUフレーム時間(VSyncを除く)が25ミリ秒、GPU時間が20ミリ秒であれば問題ありません。CPU負荷は高くても、すべてがバジェット内に収まるため、最適化を行ってもフレームレートは向上しません(CPUとGPUの両方が16.66ミリ秒未満で60 FPSに上昇しない限り)。
  • CPUフレーム時間が40ms、GPUが20msの場合、CPUバウンドであり、CPUパフォーマンスを最適化する必要があります。GPUパフォーマンスの最適化はヘルプにはなりません。実際、例えば、必要に応じてC#コードの代わりにコンピュートシェーダーを使用してバランスを調整するなど、CPU処理の一部をGPUに移行することをお勧めします。
  • CPUフレーム時間が20ms、GPUが40msの場合、GPUバウンドであり、GPU処理を最適化する必要があります。
  • CPUとGPUの両方が40ミリ秒の場合、両方に縛られ、30 FPSに到達するには両方を33.33ミリ秒未満に最適化する必要があります。

CPU バウンドまたは GPU バウンドであることをさらに探る次のリソースを参照してください。

フレームバジェットに収まっているか?

フレームバジェットに収まっているか?

開発の初期段階から頻繁にプロジェクトのプロファイリングと最適化を行うことで、アプリケーションのすべてのCPUスレッドとGPU全体のフレーム時間をフレームバジェット内に収めるのに役立ちます。このプロセスを導く質問は、フレームバジェットの範囲内に収まっているかどうかです。

上の画像は、継続的なプロファイリングと最適化を行ったチームによって開発されたUnityモバイル ゲームのプロファイル キャプチャです。このゲームの目標は、このキャプチャのようなハイスペックのモバイル端末では 60 FPS、中程度または低スペックのモバイル端末では 30 FPS です。

選択したフレームの時間の半分近くが黄色の WaitForTargetFPS プロファイラーマーカーで占められていることに注意してください。アプリケーションで application.targetFrameRate が 30 FPS に設定され、VSync が有効になっています。メインスレッドでの実際の処理は 19 ミリ秒付近で終了し、残りの時間は 33.33 ミリ秒の経過を待ってから次のフレームを開始します。この時間はプロファイラーマーカーで表されますが、この時間は実質的にメイン CPU スレッドがアイドル状態であるため、CPU は冷却されて最小限のバッテリ電力しか消費されません。

注意すべきマーカーが他のプラットフォームで異なる場合や、VSync が無効になっている場合。重要なのは、メイン スレッドがフレーム バジェット内で実行されているか、フレーム バジェット内で正確に実行されているかを、アプリケーションがVSyncを待機していることを示す何らかのマーカーと、他のスレッドにアイドル時間があるかどうかをチェックすることです。

アイドル時間はグレーまたは黄色のプロファイラーマーカーで表されます。上のスクリーンショットは、レンダースレッドが Gfx.WaitForGfxCommandsFromMainThread で停止していることを示しています。これは、1 フレームで GPU への描画コールの送信を終了し、次のフレームで CPU からのさらなるドローコール要求を待機している時間を示しています。同様に、Job Worker 0スレッドはCanvas.GeometryJobでしばらく過ごしますが、ほとんどの時間はアイドル状態です。これらはすべて、フレームバジェット内に余裕のあるアプリケーションがあることを示しています。

ゲームがフレームバジェットの場合

バッテリー使用量とサーマルスロットリングを考慮したバジェットの調整を含め、フレーム バジェットの範囲内であれば、キーのプロファイリング タスクは完了です。最後に、Memory Profiler を実行して、アプリケーションもメモリバジェット内に収まるようにします。

上の画像は、30 FPS に必要な約 22 ミリ秒のフレームバジェット内でスムーズに実行されているゲーム。WaitForTargetfps は、VSync までのスレッド時間と、レンダースレッドとワーカースレッドのグレーのアイドル時間をパディングしていることに注意してください。また、VBlank間隔は、フレーム上のGfx.Presentフレームの終了時間を見ることで確認できます。Timeline領域または上部のTimeルーラーに時間スケールを描画して、これらの時間スケールの1つから次の時間スケールまでを測定することができます。

CPU バウンド

CPUバウンド

ゲームが CPU フレームバジェット内に収まらない場合、次のステップは CPU のどの部分がボトルネックになっているか、つまりどのスレッドが最もビジー状態になっているかを調査することです。

CPU ワークロード全体がボトルネックになることはまれです。最近の CPU には、独立して同時に処理を実行することができるさまざまなコアがあります。各 CPU コアで異なるスレッドを実行できます。Unity アプリケーション全体では、さまざまな目的で範囲のスレッドを使用しますが、パフォーマンスの問題を見つけるために最もよく見られるのは、次のようなスレッドです。

  • メインスレッド:ここで、ゲームロジック/スクリプトの大部分がデフォルトで処理を実行します。物理演算、アニメーション、UI、レンダリングの初期段階など、ほとんどのUnityシステムはここで実行されます。
  • レンダリングスレッド:これは、GPUにレンダリング命令を送信する前に行う必要がある準備作業(シーン内のどのオブジェクトがカメラに見えているか、視錐台の外側にあるか、遮蔽されているか、または他の基準によってカリングされているために除外/不可視になっているかなど)を処理します。
  • レンダリングプロセス中、メインスレッドはシーンを調べ、カメラカリング、深度ソート、ドローコールバッチ処理を実行して、レンダリングする要素のリストを生成します。このリストはレンダースレッドに渡され、Unityの内部プラットフォームに依存しない表現から、特定のプラットフォームでGPUに指示するために必要な特定のグラフィックスAPI呼び出しに変換されます。
  • ジョブ ワーカー スレッド:開発者は、ジョブシステムを利用して、ワーカースレッドで実行するある種の作業をスケジュール設定できるため、メインスレッドのワークロードを軽減できます。物理演算、アニメーション、レンダリングなど、Unityのシステムや機能の一部でもジョブ システムを利用しています。

メインスレッド最適化の実例

下の画像は、メインスレッドでバウンドされたプロジェクト内の様子を示しています。このプロジェクトはMeta Quest 2で実行しています。これは通常、13.88ミリ秒(72 FPS)、さらには8.33ミリ秒(120 FPS)のフレーム予算を目標としています。これは、VRデバイスで乗り物酔いを防ぐためには高フレームレートが重要であるためです。しかし、このゲームが 30 FPS を目標としていたとしても、このプロジェクトが困難な状況にあることは消去できます。

レンダースレッドとワーカースレッドはフレームバジェット内の例と似ていますが、メインスレッドはフレーム全体の処理で明らかにビジー状態です。フレーム終了時のプロファイラーのわずかなオーバーヘッドを考慮しても、メインスレッドは 45 ミリ秒以上ビジー状態であるため、このプロジェクトのフレームレートは 22 FPS 未満です。メインスレッドが VSync を待機していることを示すマーカーはありません。フレーム全体にわたってビジー状態です。

次の調査段階では、フレームの中で最も時間がかかる部分を特定し、その理由を理解します。このフレームでは、PostLateUpdate.FinishFrameRendering に 16.23 ミリ秒かかり、これはフレームバジェット全体を上回っています。よく見ると、Inl_RenderCameraStack というマーカーのインスタンスが 5 つあり、5 つのカメラがアクティブでシーンをレンダリングしていることがわかります。Unityのすべてのカメラは、カリング、ソート、バッチ処理を含むレンダーパイプライン全体を呼び出すため、このプロジェクトの最優先タスクは、アクティブなカメラの数を1台に減らすことです(理想的)。

すべての MonoBehaviour.Update() メソッドの実行を含むプロファイラーマーカー BehaviourUpdate は、このフレームで 7.27 ミリ秒かかります。

Timeline ビューのマゼンタ色の部分は、スクリプトがマネージヒープを割り当てているポイントを示します。Hierarchy ビューに切り替え、検索バーに GC.Alloc と入力してフィルタリングすると、このフレームでこのメモリの割り当てに約 0.33 ミリ秒かかることがわかります。ただし、これはメモリ割り当てが CPU パフォーマンスに与える影響を測るための不正確な指標です。

GC.Alloc マーカーは、一般的なプロファイラーサンプルのように、開始点と終了点を記録して時間を計測しません。オーバーヘッドを最小化するために、Unity は割り当てのタイムスタンプと割り当てサイズのみを記録します。

プロファイラーは、プロファイラーのビューに表示されるようにするためにだけ、GC.Alloc マーカーに人為的に小さいサンプル継続時間を割り当てます。実際の割り当ては、特にシステムから新しい範囲のメモリを要求する必要がある場合は、より時間がかかることがあります。影響をより明確に確認するには、割り当てを行うコードの周囲にプロファイラーマーカーを配置します。ディーププロファイリングでは、Timeline ビューのマゼンタ色の GC.Alloc サンプルの隙間が所要時間の目安になります。

さらに、新しいメモリを割り当てると、パフォーマンスに悪影響を及ぼし、直接測定して特定するのが難しくなる可能性があります。

  • システムに新しいメモリを要求すると、モバイルデバイスのパワーバジェットに影響し、システムの CPU または GPU の速度が低下する可能性があります。
  • 新しいメモリは、CPU の L1 キャッシュにロードされ、既存のキャッシュラインを押し出す必要があります。
  • インクリメンタルガベージコレクションは、マネージドメモリの既存の空き領域を最終的に超えたときに、直接トリガーされるか、遅延してトリガーされます。

フレームの開始時に、Physics.FixedUpdate のインスタンスが 4 つ追加されると、合計で 4.57 ミリ秒になります。あとで、LateBehaviourUpdate (MonoBehaviour.LateUpdate() の呼び出し) にかかる時間は 4 ミリ秒、Animator アカウントは約 1 ミリ秒です。このプロジェクトが目標のフレームバジェットとレートに到達するためには、これらのメインスレッドの問題をすべて調査し、適切な最適化を見つける必要があります。

メインスレッドのボトルネックに関するよくある落とし穴

最も時間がかかる部分を最適化することで、パフォーマンスが最大に向上します。メインスレッドバウンドのプロジェクトでは、多くの場合、次の領域の最適化を検討することをお勧めします。

  • 物理演算
  • MonoBehaviour スクリプトの更新
  • ガベージ割り当て/ガベージコレクション
  • カリングとメインスレッドでのレンダリング
  • 非効率的なドローコールバッチ処理
  • UI の更新、レイアウト、再構築
  • アニメーション

最適化ガイドでは、最も一般的な落とし穴のいくつかを最適化するための実用的なヒントの長いリストを提供しています。

調査する問題によっては、他のツールも役立つことがあります。

  • 時間がかかるが、その理由がわからない MonoBehaviour スクリプトの場合は、コードにプロファイラーマーカーを追加するか、ディーププロファイリングを試してコールスタック全体を確認してください。
  • マネージ メモリを割り当てるスクリプトの場合は、割り当てコール スタックを有効にして、割り当てがどこから来ているかを正確に確認します。または、ディーププロファイリングを有効にするか、Project Auditor を使用します。これは、メモリによってフィルタリングされたコード問題を表示するので、マネージ割り当ての結果生じるすべてのコード行を特定できます。
  • フレームデバッガーを使用して、ドローコールバッチ処理がうまくいかなかった原因を調査します。

ゲームの最適化に関する包括的なヒントについては、以下のUnityエキスパートガイドを無料でダウンロードしてください。

CPUバウンド:レンダースレッド

CPUバウンド:レンダースレッド

これは、レンダースレッドによってバウンドされている実際のプロジェクトです。これは等角視点のコンソールゲームで、ターゲットフレームバジェットは 33.33 ミリ秒です。

プロファイラーのキャプチャは、現在のフレームでレンダリングを開始する前に、メインスレッドがレンダースレッドを待機することを示しています。これは、 Gfx.WaitForPresentOnGfxThread マーカーで示されています。レンダースレッドは、前のフレームからのドローコールコマンドをまだ送信しており、メインスレッドからの新しい描画コールを受け入れる準備ができていません。また、Camera.Renderに時間を費やしています。

現在のフレームに関連するマーカーと、他のフレームのマーカーは暗く見えるため、見分けがつきます。また、メインスレッドが続行して、レンダースレッドが処理するための描画呼び出しの発行を開始できると、レンダースレッドが現在のフレームを処理するのに 100 ミリ秒以上かかり、次のフレームの間にもボトルネックが発生することがわかります。

さらに調査を進めると、このゲームは 9 つの異なるカメラを使用し、シェーダーの交換によって多くの余分なパスが発生する複雑なレンダリング設定になっていることがわかりました。また、このゲームでは、フォワードレンダリングパスを使用して130を超えるポイントライトをレンダリングしていました。フォワードレンダリングパスでは、各ライトに複数の透過描画コールを追加できます。これらの問題が組み合わさって、1 フレームあたり 3,000 を超える描画コールが作成されました。

レンダースレッドのボトルネックに関するよくある落とし穴

レンダースレッドバウンドのプロジェクトを調査する一般的な原因を次に示します。

  • ドローコールバッチ処理不足:これは特に、OpenGL や DirectX 11 などの古いグラフィックス API に当てはまります。
  • カメラが多すぎます:分割画面の Multiplayer ゲームを制作している場合を除き、アクティブなカメラは 1 つだけにする必要があります。
  • 不十分なカリング:その結果、描画される要素が多すぎます。カメラの錐台寸法とカリンレイヤーマスクを調査します。

レンダリングプロファイラーモジュールは、毎フレームのドローコールバッチと SetPass コールの数の概要を表示します。レンダースレッドが GPU に発行しているドローコールバッチを調べるための最良のツールはフレームデバッガーです。

特定されたボトルネックを解決するためのツール

このeBookでは、パフォーマンスの問題を特定することに重点を置いていますが、以前取り上げた2つの補完的なパフォーマンス最適化ガイドでは、ターゲットプラットフォームがPC、コンソールモバイルのいずれであるかに応じて、ボトルネックの解決方法を提案しています。レンダースレッドのボトルネックに関しては、Unityが提供しているバッチ処理システムとオプションは、特定した問題によって異なることを強調します。ここでは、いくつかのオプションについて簡単に説明します。詳細については、e ブックを参照してください。

  • SRPバッチ処理は、GPUメモリにマテリアル データを永続的に保存することで、CPUオーバーヘッドを削減します。実際のドローコール数は減りませんが、各ドローコールは安価になります。
  • GPUインスタンシングは、同じマテリアルを使用する同じメッシュの複数のインスタンスを1つのドローコールに統合します。
  • 静的バッチ処理は、同じマテリアルを共有する静的 (非移動) メッシュを組み合わせるため、静的要素の多いレベルデザインを扱う場合に有効です。
  • GPU常駐ドロワーは、類似のゲームオブジェクトをグループ化することで、自動的にGPUインスタンシングを使用してCPUオーバーヘッドと描画コールを削減します。
  • 動的バッチ処理では、ランタイム時に小さなメッシュが結合されるため、ドローコールコストの高い旧式のモバイルデバイスでは有利になることがあります。ただし、頂点変換は多くのリソースを必要とする可能性があるという欠点もあります。
  • GPUオクルージョンカリングは、コンピュート シェーダーを使用して、現在と前のフレームの深度バッファを比較することでオブジェクト可視性を判断し、事前にベイクされたデータを必要とせずに、オクルージョンされたオブジェクトの不要なレンダリングを削減します。

また、CPU 側では、Camera.layerCullDistances などの手法を使用して、カメラからの距離に基づいてオブジェクトをカリングすることで、レンダースレッドに送信されるオブジェクトの数を減らすことができ、カメラカリング中の CPU ボトルネックを軽減できます。

これらは利用可能なオプションのほんの一部です。それぞれの長所と短所があります。一部のプラットフォームに限定されます。プロジェクトでは、多くの場合、これらのシステムをいくつか組み合わせて使用し、それらを最大限に活用する方法を理解する必要があります。

CPUバウンド:ワーカースレッド

CPUバウンド:ワーカースレッド

メインスレッドやレンダースレッド以外の CPU スレッドに束縛されているプロジェクトはそれほど一般的ではありません。ただし、プロジェクトで DOTS (Data-Oriented Technology Stack) が使用されている場合、特にジョブシステムを使用してメインスレッドからワーカースレッドに処理が移動される場合に発生する可能性があります。

上の画像は、エディターの再生モードのキャプチャで、DOTS プロジェクトが CPU で粒子流体シミュレーションを実行している様子を示しています。

一見すると成功のように見える。ワーカー スレッドはBurstコンパイルされたジョブで密に詰め込まれており、大量の処理がメイン スレッドから移動されたことを示しています。通常、これは正しい決断です。

ただし、この場合、フレーム時間が 48.14 ms、グレーの WaitForJobGroupID マーカーが 35.57 ms のメインスレッドは、すべてがうまくいっていないことを示しています。WaitForJobGroupID は、メインスレッドがワーカースレッドで非同期的に実行されるジョブをスケジュールしたことを示しますが、ワーカースレッドの実行が終了する前に、それらのジョブの結果が必要です。WaitForJobGroupID の下にある青いプロファイラーマーカーは、メインスレッドがジョブをより速く終了するために、待機中にジョブを実行していることを示します。

ジョブは Burst コンパイルされていますが、まだ多くの処理が行われています。このプロジェクトで、どのパーティクルが近くにあるかをすばやく見つけるために使用される空間クエリ構造体は、より効率的な構造体にするために最適化またはスワップする必要があるかもしれません。また、空間クエリは開始ではなくフレームの終わりにスケジュールでき、結果は次のフレームの開始まで不要です。このプロジェクトでは、シミュレートしようとしているパーティクルが多すぎるのかもしれません。解決策を見つけるにはジョブのコードをさらに分析する必要があるため、より細かいプロファイラーマーカーを追加することで、最も遅い部分を特定するのにヘルプます。

プロジェクト内のジョブは、この例ほど並列化されていない可能性があります。1 つのワーカースレッドで 1 つの長いジョブを実行するだけという場合もあります。ジョブがスケジュールされてから完了までの時間が、ジョブを実行するのに十分な長さであれば問題ありません。そうでない場合は、上のスクリーンショットのように、ジョブの完了を待っている間にメイン スレッドがストールします。

ワーカースレッドボトルネックのよくある落とし穴

同期ポイントとワーカースレッドのボトルネックの一般的な原因は次のとおりです。

  • Burst コンパイラーによってコンパイルされていないジョブ
  • 複数のワーカースレッドで並列されるのではなく、1つのワーカースレッドで長時間実行されるジョブ
  • フレーム内のジョブがスケジュールされた時点から結果が必要な時点までの時間不足
  • すべてのジョブを即座に完了する必要がある、フレーム内の複数の「同期ポイント」

CPU 使用状況プロファイラーモジュールの Timeline ビューのフローイベント特徴を使用すると、ジョブがスケジュールされるタイミングと、メインスレッドによってその結果が予期されるタイミングを調査できます。

効率的な DOTS コードの記述の詳細については、DOTS ベストプラクティスガイドを参照してください。

GPU バウンド

GPU バウンド

メインスレッドが Gfx.WaitForPresentOnGfxThread などのプロファイラーマーカーに長時間を費やし、レンダースレッドが Gfx.PresentFrame<GraphicsAPIName>.WaitForLastPresent などのマーカーを同時に表示している場合、アプリケーションは GPU 依存です

GPUフレーム時間を取得する最善の方法は、ターゲット プラットフォーム固有のGPUプロファイリング ツールを使用することですが、すべてのデバイスで信頼性の高いデータを簡単にキャプチャできるわけではありません。

FrameTimingManager API は、CPU と GPU の両方で低オーバーヘッドで高レベルのフレーム時間を提供する、そのような場合に便利です。

上のキャプチャは、Androidモバイル端末でVulkanグラフィックスAPIを使用して撮影したものです。この例の Gfx.PresentFrame で費やされた時間の一部は VSync の待機に関連している可能性がありますが、このプロファイラーマーカーの極端な長ささは、この時間の大部分が GPU が前のフレームのレンダリングを終了するのを待っていることを示しています。

このゲームでは、特定のゲームプレイイベントがシェーダーの使用をトリガーし、GPU によってレンダリングされる描画コールの数が 3 倍になりました。GPUパフォーマンスのプロファイリング時に調査すべき一般的な問題は次のとおりです。

  • アンビエントオクルージョンやブルームなどの高コストな画面のポストプロセスエフェクト
  • フラグメントシェーダーのコストが高い原因:
  • シェーダーコード内の分岐ロジック
  • 特にモバイルでは、半精度ではなく完全 Float 精度を使用する
  • GPU のウェーブフロント占有率に影響を与えるレジスタの過剰な使用
  • トランスペアレントレンダーキューでのオーバードローの原因:
  • 非効率的な UI レンダリング
  • パーティクルシステムの重複または過剰な使用
  • ポストプロセッシングエフェクト
  • 次のような極端に高い画面解像度
  • 4K ディスプレイ
  • モバイルデバイスでのRetinaディスプレイ
  • 微小三角形の原因:
  • 高密度メッシュジオメトリ
  • LOD(LOD)システムの欠如。モバイル GPU では特に問題だが、PC やコンソール GPU にも影響を与える可能性がある
  • キャッシュミスと無駄な GPU メモリ帯帯域幅の原因:
  • 非圧縮テクスチャ
  • ミップマップを使用しない高解像度テクスチャ
  • ジオメトリシェーダーまたはテッセレーションシェーダー。ダイナミックシャドウが有効になっている場合は、フレームごとに複数回実行される可能性があります。

アプリケーションが GPU バウンドであると思われる場合は、フレームデバッガーを使用して、GPU に送信されるドローコールバッチを簡単に理解できます。ただし、このツールは特定のGPUタイミング情報を提示できず、シーン全体の構成方法のみを提示します。

GPU のボトルネックの原因を調査する最善の方法は、適切な GPU プロファイラーからの GPU キャプチャを調べることです。使用するツールは、ターゲットハードウェアと選択したグラフィックス API によって異なります。詳細については、e ブックのプロファイリングおよびデバッグツールのセクションを参照してください。

Unity ベストプラクティスガイド
Unity開発者およびクリエイター向けのその他のヒント

Unityのベストプラクティスハブから、その他のベストプラクティスやヒントを見つけることができます。Unity のツールセットやシステムを使って効率的に開発するのにヘルプ、業界のエキスパートや Unity のエンジニア、テクニカルアーティストが作成した 30 以上のガイドからお選びください。

ゲームのパフォーマンスのプロファイリングのベストプラクティス | Unity