一般的な最適化のTips

はじめに

理想的な世界では、コンピューターは無限の速度で動作します。実現可能なことの唯一の限界は私たちの想像力です。しかし現実の世界では、最速のコンピューターでさえも機能不全に陥らせるソフトウェアをいとも簡単に作成できます。

したがって、ゲームやその他のソフトウェアの設計する際は、実現したいことと、良好なパフォーマンスのバランスを取り、現実的に達成できる妥協点を検討する必要があります。

最良の結果を得るために、次の2つのアプローチがあります。

  • より速く動かす。

  • よりスマートに動かす。

できればこの2つを両方とも達成できればベストです。

ごまかし

ゲームではプレイヤーに実際よりも、はるかに複雑で、インタラクティブで、グラフィックが刺激的な世界にいると信じ込ませることができることを認識する必要があります。よりスマートに仕事をするためには、優れたプログラマーはマジシャンであり、業界の最新技術を習得しながら、新しい技術を発明するように努めるべきです。

遅さの本質

外部から見ると、パフォーマンスの問題は多くの場合、ひとまとめにされています。しかし、実際にはパフォーマンスの問題にはいくつかの種類があります。

  • 毎フレーム発生する重い処理で、フレーム レートが継続的に低くなっている。

  • 断続的な重い処理により「スパイク」が発生し、アニメーションの停止につながっている。

  • レベルをロードするときなど、通常のゲームプレイ以外で発生する重い処理。

これらはいずれもユーザーにとって煩わしいものですが、煩わしさの度合いはそれぞれ異なります。

パフォーマンスの測定

おそらく、最適化のための最も重要なツールは、パフォーマンスを測定する機能です。つまりボトルネックがどこにあるかを特定し、速度向上の試みが成功したかどうかを測定します。

パフォーマンスを測定する方法はいくつかあります。

異なる領域の相対的なパフォーマンスは、ハードウェアによって異なる場合があることに注意してください。複数のデバイスで測定することは、多くの場合良いアイデアです。これは特にモバイルデバイスをターゲットにしている場合に当てはまります。

制限事項

CPU プロファイラーは、パフォーマンスを測定するためのよく使われる方法です。ただし必ずしも全体像がわかるわけではありません。

  • CPU によって与えられた命令の「結果的に」、ボトルネックが GPU に発生することがよくあります。

  • Godot で使用される命令 (たとえば動的メモリ割り当て) の「結果的に」、OSのプロセス (Godot 外部) でスパイクが発生する可能性があります。

  • 初期設定が必要なため、携帯電話などの特定のデバイスのプロファイルを作成できない場合があります。

  • アクセスできないハードウェアで発生するパフォーマンスの問題を解決しなければならない場合があります。

これらの制限の結果として、ボトルネックがどこにあるかを見つけるために探偵のような調査が必要になることがよくあります。

探偵のような調査

探偵のような調査は、開発者にとって非常に重要なスキルです (パフォーマンス改善の際でも、バグ修正の際でも)。これには仮説検証や二分検索が含まれます。

仮説と検証

たとえば、スプライトがゲームの速度を低下させていると考えているとします。この仮説は次のようにして検証できます:

  • スプライトを追加したり、いくつか削除したりしたときのパフォーマンスの変化を測定します。

これはさらなる仮説につながるかもしれません: スプライトのサイズによってパフォーマンスの低下が起きるかも?

  • これを検証するには、スプライトのサイズを変更し、パフォーマンスを測定します。

プロファイラー

プロファイラーを使用すると、プログラムの実行時間を計測できます。プロファイラーはさまざまな関数やルーチンに費やされた時間の割合や、関数が呼び出された頻度を示す結果を提供します。

これはボトルネックを特定し、改善の結果を測定するのに非常に役立ちます。パフォーマンスを改善しようとすると、裏目に出てパフォーマンスが低下することがあります。 常にプロファイリングとタイミングを使用してあなたの取り組みを導いてください。

Godot の組み込みプロファイラーの使用に関する詳細については プロファイラー を参照してください。

最適化の原則

Donald Knuth 曰く:

プログラマーはプログラムの重要でない部分の速度について考えたり心配したりすることに膨大な時間を費やしています。そして、こうした効率化の試みは、デバッグやメンテナンスを考慮すると、実際には大きなマイナスの影響を及ぼします。私たちはたとえば 97% の時間、小さな効率化については忘れるべきです。早まった最適化は諸悪の根源です。しかし、その重要な 3% の機会を逃すべきではありません。

このメッセージは非常に重要です:

  • 開発者の時間は限られています。盲目的にプログラムのすべての側面を高速化しようとするのではなく、本当に重要な側面に集中して取り組む必要があります。

  • 最適化の取り組みにより、最適化されていないコードよりも読みにくくデバッグしにくいコードになってしまうことがよくあります。これを本当にメリットのある領域に限定することが、チームの利益になります。

特定のコード部分を最適化できるからといって、必ずしも最適化すべきというわけではありません。いつ最適化すべきでいつ最適化すべきでないかを知ることは、習得すべき素晴らしいスキルです。

この引用文で誤解を招く点の 1 つは、人々がサブ引用文の「時期尚早な最適化は諸悪の根源である」に注目する傾向があることです。時期尚早な最適化は (定義上) 望ましくありませんが、パフォーマンスの高いソフトウェアはパフォーマンスの高い設計から生まれます。

パフォーマンスの高い設計

必要になるまで最適化を無視するように人々に勧めることの危険性は、パフォーマンスを考慮する最も重要な時期は、キーがキーボードに入力される前の設計段階であることを都合よく無視することです。プログラムの設計やアルゴリズムが非効率的であれば、後で細部をいくら磨いても、プログラムの実行速度は上がりません。いくらか実行速度は速くなるかもしれませんが、パフォーマンスを重視して設計されたプログラムと同じ速度で実行されることは決してありません。

これは一般的なプログラミングよりも、ゲームやグラフィックスのプログラミングにおいて、はるかに重要になる傾向があります。パフォーマンスの高い設計は、低レベルの最適化がなくても、低レベルの最適化が行われた平凡な設計よりも何倍も高速に実行されることがよくあります。

インクリメンタル設計

もちろん、実際には事前の知識がなければ、最初から最高の設計を思いつく可能性は低いでしょう。その代わりに多くの場合、特定のコード領域のバージョンをいくつか作成し、特定のコード領域について、それぞれ異なるアプローチで何バージョンも作成することがよくあります。全体的な設計が完成するまで、この段階では詳細に時間をかけすぎないことが重要です。そうしないと作業の大部分が無駄になってしまいます。

パフォーマンスの高い設計の一般的なガイドラインを示すことは困難です。これは問題に大きく依存するからです。しかし CPU 側で言及する価値のある1つのポイントは、最近の CPU は常にメモリ帯域幅によって性能が制限されているということです。このためメモリ内をあちこち飛び回るのではなく、データの キャッシュ局所性 と線形アクセスのためのデータ構造とアルゴリズムを設計する、データ指向設計が重要です。

最適化のプロセス

合理的な設計ができたと仮定し、Knuth から教訓を得たとして、最適化の最初のステップは、最大のボトルネック、つまり最も遅い関数、簡単に達成できる部分を特定することです。

最も遅い部分の速度を改善することができたら、そこがボトルネックではなくなる可能性があります。そのため、再度テスト/プロファイルを実行し、次に重点を置くべきボトルネックを見つける必要があります。

プロセスは次のようになります。

  1. プロファイルしてボトルネックを特定する。

  2. ボトルネックを最適化する。

  3. ステップ1に戻る。

ボトルネックの最適化

プロファイラーでは、関数のどの部分 (データアクセス、計算処理など) が速度を低下させているかがわかります。

設計と同様に、まずアルゴリズムとデータ構造が最善のものとなるよう努力を集中する必要があります。データアクセスはローカルで行いましょう (CPU キャッシュを最大限に活用するため)。またデータのコンパクトなストレージを使用する方がよい場合が多くあります (ここでも常にプロファイルを作成して結果をテストしてください)。多くの場合、負荷の高い計算は事前に計算しましょう。これはレベルをロードするときに計算を実行したり、事前に計算されたデータが記録されているファイルをロードしたり、複雑な計算の結果をスクリプト定数に格納してその値を使用することなどで実行できます。

アルゴリズムとデータが適切であれば、ルーチンに小さな変更を加えるだけでパフォーマンスが向上することがよくあります。たとえば、一部の計算をループ外に移動したり、ネストされた for ループをネストされていないループに変換したりできます。(2D 配列の幅または高さが事前にわかっている場合は、これが実行可能なはずです。)

変更を加えた後は必ずタイミング/ボトルネックを再テストしてください。変更によっては速度が向上するものもありますが、マイナスの影響を与えるものもあります。場合によっては、わずかなプラスの効果を、より複雑なコードによるマイナスの影響が上回ることがあり、その最適化を省くことを選択することもあります。

付録

ボトルネックの計算

「鎖の強さは最も弱い部分と同じ」 という諺は、パフォーマンスの最適化に直接当てはまります。プログラムの時間の 90% が関数「A」に費やされている場合、「A」を最適化するとパフォーマンスに大きな影響を与える可能性があります。

A: 9 ms
Everything else: 1 ms
Total frame time: 10 ms
A: 1 ms
Everything else: 1ms
Total frame time: 2 ms

この例では、このボトルネック「A」を 9 倍改善すると、全体のフレーム時間が 5 倍短縮され、1 秒あたりのフレーム数が 5 倍増加します。

ただし、他の何かの実行速度が遅く、プログラムのボトルネックになっている場合は、同じ改善を行ってもそれほど劇的な効果は得られません。

A: 9 ms
Everything else: 50 ms
Total frame time: 59 ms
A: 1 ms
Everything else: 50 ms
Total frame time: 51 ms

この例では、関数「A」を大幅に最適化しているにもかかわらず、フレームレートの点での実際の向上はわずかです。

ゲームでは CPU と GPU が互いに独立して実行されるため、状況はさらに複雑になります。合計フレーム時間は、2つのうち遅い方によって決まります。

CPU: 9 ms
GPU: 50 ms
Total frame time: 50 ms
CPU: 1 ms
GPU: 50 ms
Total frame time: 50 ms

この例では、CPU を再度大幅に最適化しましたが、GPU がボトルネックになっているため、フレーム時間は改善されませんでした。