CPUの最適化
パフォーマンスの測定
プログラムを高速化するには、「ボトルネック」がどこにあるかを知る必要があります。ボトルネックとは、プログラムの中で最も遅い部分であり、全体の実行速度を制限します。ボトルネックに焦点を当てることで、わずかなパフォーマンス向上につながる機能の最適化に多くの時間を費やすのではなく、速度を最大限に向上できる領域の最適化に労力を集中できます。
CPU の場合、ボトルネックを特定する最も簡単な方法は、プロファイラーを使用することです。
CPUプロファイラー
プロファイラーはプログラムと並行して実行され、タイミング測定を行って各関数に費やされる時間の割合を計算します。
Godot IDE には便利な組み込みプロファイラーがあります。プロジェクトを開始するたびに実行されるわけではなく、手動で開始および停止する必要があります。これはほとんどのプロファイラーと同様に、これらのタイミング測定を記録するとプロジェクトが大幅に遅くなる可能性があるためです。
プロファイリングの後、フレームの結果を確認することができます。
デモプロジェクトの 1 つのプロファイルの結果。
注釈
物理やオーディオなどの組み込みプロセスのコストを確認できるほか、下部には独自のスクリプト関数のコストも表示されます。
さまざまな組み込みサーバーの待機に費やされた時間は、プロファイラーではカウントされない可能性があります。これは既知のバグです。
プロジェクトの実行速度が遅い場合、他の機能やプロセスよりも明らかに多くの時間がかかっていることがわかります。これが主なボトルネックであり、通常はこの領域を最適化することで速度を上げることができます。
Godot の組み込みプロファイラーの詳細な使用方法については、 デバッガーパネル を参照してください。
外部のプロファイラー
Godot IDE プロファイラーは非常に便利で役立ちますが、場合によっては、より強力なプロファイラー機能や、Godot エンジン自体をプロファイリングすることが必要になることがあります。
これを行うには、いくつかの サードパーティ製 C++ プロファイラー を使用できます。
Valgrind の一部である Callgrind の結果。
Callgrind は、左から関数とその子関数内の時間の割合 (Inclusive)、子関数を除いた関数自体内で費やされた時間の割合 (Self)、関数が呼び出された回数、関数名、ファイルまたはモジュールを表示します。
この例では、ほぼすべての時間が Main::iteration() 関数で費やされていることがわかります。これは Godot ソースコード内で繰り返し呼び出されるマスター関数です。この関数はフレームの描画、物理ティックのシミュレート、ノードとスクリプトを更新を行います。この例では 2D ベンチマークを使用しているため、時間の大部分 (66%) はキャンバスをレンダリングする関数に費やされています。その下には、時間のほぼ 50% が libglapi と i965_dri (グラフィックス ドライバー) といった、 Godot コード外で費やされていることがわかります。これは CPU 時間の大部分がグラフィックスドライバーに費やされていることを示しています。
これは興味深い事例です。グラフィックスドライバーに費やされる時間は最小にするのが理想的です。これはグラフィックスAPIで行われる通信や作業が多すぎるという問題があることを示しています。この具体的なプロファイリングが、2Dバッチングの開発につながりました。この分野のボトルネックを減らすことで、2Dレンダリングが大幅に高速化されます。
タイム関数で実行時間を測定する
プロファイラーを使用してボトルネックを特定した後は、テスト対象の関数またはコードの実行時間を手動で測定するという便利なテクニックもあります。具体的な方法は言語によって異なりますが、GDScript では次の操作を行います。
var time_start = Time.get_ticks_usec()
# Your function you want to time
update_enemies()
var time_end = Time.get_ticks_usec()
print("update_enemies() took %d microseconds" % time_end - time_start)
var timeStart = Time.GetTicksUsec();
// Your function you want to time.
UpdateEnemies();
var timeEnd = Time.GetTicksUsec();
GD.Print($"UpdateEnemies() took {timeEnd - timeStart} microseconds");
関数の時間を手動で計測する場合、通常は関数を 1 回だけではなく、何度も (1,000 回以上) 実行することをお勧めします (非常に遅い関数の場合を除く)。これを行う理由はタイマーの精度が限られていることが多く。さらに CPU はプロセスを無計画にスケジュールするため結果がぶれることが多いためです。一連の実行の平均を取ることで、 1 回だけ測定するよりも正確になります。
関数の最適化を試みる際には、必ず繰り返しプロファイリングするか、実行中に時間を計ってください。これにより、最適化が機能しているかどうかに関する重要なフィードバックが得られます。
CPUキャッシュ
CPU キャッシュは、特に関数の 2 つの異なるバージョンのタイミング結果を比較する場合に特に注意する必要があるもう 1 つの要素です。測定結果はデータが CPU キャッシュにあるかどうかに大きく依存する可能性があります。CPU キャッシュと比較してシステム RAM が非常に大きい (数メガバイトではなく数ギガバイト) にもかかわらず、CPU はシステム RAM から直接データをロードしません。これはシステム RAM へのアクセスが非常に遅いためです。代わりに CPU はキャッシュと呼ばれる、より小さく、より高速なメモリバンクからデータをロードします。キャッシュからのデータのロードは非常に高速ですが、キャッシュに格納されていないメモリ アドレスをロードしようとするたびに、キャッシュはメインメモリにアクセスして、データをゆっくりとロードする必要があります。この遅延により、CPU が長時間アイドル状態になる可能性があります。これを「キャッシュミス」と呼びます。
つまり、関数を初めて実行すると、データが CPU キャッシュにないため、実行速度が遅くなる可能性があります。2 回目以降は、データがキャッシュにあるため、実行速度が大幅に速くなる可能性があります。このため、実行時間を計測する際は常に平均値を使用し、キャッシュの影響があることに注意してください。
キャッシュを理解することは、CPU の最適化にも重要です。ランダムに分散したメインメモリ領域から小さなデータをロードするアルゴリズム (ルーチン) がある場合、キャッシュミスが頻繁に発生し、CPU が待機する時間が長くなり、何も処理できなくなります。代わりに、データアクセスをローカル化したり、メモリに線形 (連続リストなど) でアクセスしたりすれば、キャッシュが最適に機能し、CPU は可能な限り高速に処理できるようになります。
Godot は通常このような低レベル事情を考慮して処理しています。たとえば、サーバー API はレンダリングや物理などのために、データがキャッシュ用に最適化されています。それでも GDExtensions を作成するときは、キャッシュに特に注意する必要があります。
言語
Godot はさまざまな言語をサポートしていますが、トレードオフがあることを念頭に置いておく必要があります。一部の言語は速度を犠牲にして使いやすさを重視して設計されており、また他の言語は高速ですが扱いが難しいです。
組み込みエンジン関数は、選択したスクリプト言語に関係なく、同じ速度で実行されます。プロジェクトで独自のコード内で多くの計算を行っている場合は、それらの計算をより高速な言語に移行することを検討してください。
GDScript
GDScript is designed to be easy to use and iterate, and is ideal for making many types of games. However, in this language, ease of use is considered more important than performance. If you need to make heavy calculations, consider moving some of your project to one of the other languages.
C#
C# is popular and has first-class support in Godot. It offers a good compromise between speed and ease of use. Beware of possible garbage collection pauses and leaks that can occur during gameplay, though. A common approach to workaround issues with garbage collection is to use object pooling, which is outside the scope of this guide.
他の言語
サードパーティは Rust を含む、他にもいくつかの言語のサポートを提供しています。
C++
Godot は C++ で書かれています。C++ を使用すると一般的に最も高速なコードになります。ただし実用レベルでは、異なるプラットフォーム上のエンドユーザーのマシンに展開するのが最も困難な言語です。C++ を使用する用途には、GDExtensions と カスタムモジュール があります。
スレッド
並列に実行できる計算を大量に行う場合は、スレッドの使用を検討してください。最新の CPU には複数のコアがあり、各コアは限られた量の作業を実行できます。タスクを複数のスレッドに分散することで、CPU の効率をさらに高めることができます。
スレッドの欠点は、非常に注意しなければならないことです。各 CPU コアは独立して動作するため、同時に同じメモリにアクセスしようとする可能性があります。1 つのスレッドが変数を読み込んでいる間に、別のスレッドが書き込みを行っている可能性があります。これは 競合状態 と呼ばれます。スレッドを使用する前に、この危険性と競合状態を防ぐ方法を理解しておく必要があります。スレッドを使用すると、デバッグがかなり難しくなる可能性があります。
スレッドの詳細については、複数のスレッドの使用 を参照してください。
シーンツリー
ノードは非常に強力で多用途な概念ですが、すべてのノードにはコストがかかることに注意してください。 _process() や _physics_process() などの組み込み関数はツリー全体に伝播します。この便利な仕組みによりノードの数が非常に多い場合、パフォーマンスが低下する可能性があります (正確な数はターゲットプラットフォームによって異なり、数千から数万の範囲になる可能性があるため、開発中にすべてのターゲットプラットフォームでパフォーマンスプロファイルを行うようにしてください)。
Godot レンダラーでは、各ノードが個別に処理されます。そのため、各ノードの数が少ないほど、パフォーマンスが向上します。
SceneTree の奇妙な点は、ノードを一時停止したり非表示にしたりするよりも、SceneTree からノードを削除した方がパフォーマンスが大幅に向上する場合があるということです。切り離されたノードを削除する必要はありません。たとえば、ノードへの参照を保持し、Node.remove_child(node) を使用してシーンツリーから切り離し、後で Node.add_child(node) を使用して再接続することができます。これは例えばゲームのステージにエリアを追加したり削除したりするときに非常に便利です。
サーバー API を使用すると、SceneTree を完全に回避できます。詳細については サーバーを使用した最適化 を参照してください。
物理
状況によっては、物理がボトルネックになることがあります。これは複雑な世界や、多数の物理オブジェクトがある場合に特に当てはまります。
物理を高速化するテクニックをいくつか紹介します。
衝突シェイプはレンダリングされたジオメトリの簡略化されたバージョンを使用してみてください。多くの場合、これはエンドユーザーには気付かれませんが、パフォーマンスを大幅に向上させることができます。
オブジェクトが視界外または現在のエリア外にある場合は物理からオブジェクトを削除するか、物理オブジェクトを再利用してみてください (たとえば、エリアごとに 8 体のモンスターのみ許可し、これらを再利用します)。
物理エンジンのもう 1 つの重要な側面は、物理ティックレートです。ゲームによってはティックレートを大幅に削減して、たとえば物理を 1 秒あたり 60 回更新するのではなく、1 秒あたり 30 回または 20 回だけ更新することもできます。これにより CPU 負荷を大幅に軽減できます。
物理ティックレートを変更することの欠点は、物理更新のレートがレンダリングされる 1 秒あたりのフレーム数と一致しない場合、ぎくしゃくした動きやジッターが発生する可能性があることです。また物理ティックレートを下げると、入力遅延が増加します。リアルタイムのプレイヤー移動を特徴とするほとんどのゲームでは、デフォルトの物理ティックレート (60 Hz) を維持することが推奨されます。
ジッターの解決策は 固定タイムステップ補間 を使用することです。これには物理に合わせて複数のフレームにわたってレンダリングされた位置と回転を滑らかにする処理が含まれます。これを自分で実装するか、もしくは サードパーティのアドオン を使用できます。パフォーマンスの点では、補間は物理ティックを実行するのに比べて非常に安価な操作です。そして桁違いに高速なので、ジッターを減らしながらパフォーマンスを大幅に向上できます。