目次
最適化
- 構造を理解することが最適化の近道
- アプリケーションの高速化ならボトルネックがどこにあるか調べる必要あり
大きく分けて、CPU ボトルネック、GPU ボトルネックの 2種類あります。 もしパフォーマンスが上がらないなら、まずは原因がどこにあるのか調査を行います。
最適化によりどこまで速くなる余地があるのか、GPU/CPU のおおよそのピーク性能を知っておくと手を入れる場所の目安になります。
ボトルネックの例
- CPU ボトルネック
- 同期待ち
- Driver 負荷
- アプリケーション自体の負荷
- メモリ転送量超過
- GPU ボトルネック
- Shader 負荷
- Vertex 負荷
- Pixel (Fragment) 負荷
- Texture Fetch
- フィルレート制限
- メモリ帯域制限
CPU 負荷
Lock
リソースの Lock で CPU と GPU が競合すると、CPU は GPU の動作が完了するまで待つことになります。
CPU と GPU は非同期に動作しているので、 GPU は裏でまだ 1frame 前に発行されたコマンドのレンダリングを行っている可能性があります。
もしリソースの Lock 待ちが発生しているなら、できるだけお互いの動作を邪魔しないよう対策が必要です。 ダブルバッファ化を行う、読み出しが不要なら Write Only (Discard) 設定にする、Fence で実行完了を先に確認しておくなど。
Driver 負荷
各種 API を呼び出すと、ドライバーはその情報をメモリに蓄積します。
目的のひとつは、描画時に必要なステートを参照するためで、 もう一つは Query API により現在設定されているステートをアプリケーションが読み出す可能性があるためです。
記録された情報を実際に参照するのは glDrawElements (DrawIndexed) や glDrawArrays (Draw) などの Draw API です。 Draw API は現在設定されている描画に必要なステートを集めて、 GPU に送信するためのコマンドを構築します。
この描画コマンドは GPU に送るための Command Buffer (Command Queue または Push Buffer) に登録されます。
登録された Command Buffer は任意のタイミングで Kick され、 実際に GPU での描画が始まります。
つまり通常の API 呼び出しは、メモリ内にデータを記録するだけで GPU には何も送りません。 実際に GPU へ送る情報コマンドを生成するのは Draw API に集中していることになります。 (例外としてリソース転送 API もあります。)
描画が行われるまでに、ステートは何度も変更される可能性があるためです。 実際に描画に必要なのは、描画の直前に設定されているステートだけです。
また GPU コマンドの量を減らすために、前回 Command Buffer に登録したステートを保存しておいて差分だけ 再設定するような最適化も考えられます。
API が記録したステートは GPU に依存しない汎用のものです。 その後各 Command を、GPU 毎の Native な命令に変換する作業も発生します。
DirectX API が登場し、GPU が当たり前になって何世代もバージョンが上がってからも、Draw API の CPU 負荷の高さは問題となってきました。 というのも、CPU でレンダリングしていた時代はもちろん、またはコンシューマゲーム機などのゲーム専用ハードでは、 Draw API ボトルネックがほとんど存在していなかったからです。
GPU の種類が固定のゲーム専用機では、さまざまな GPU を想定する必要がありません。 ステート記録時に GPU に適した形に変換してしまうことが可能で、 描画時のステート切り替えも PC と比べると非常に低コストで実現できます。 それどころか、データを出力する時点で GPU Native な Command に事前変換することも可能でよく用いられます。 ほぼストリームのように、ロードしたあとは GPU に流しこむだけで描画できるわけです。
PC/Smartphone/Tablet のような汎用 OS の上では、 最適化のためには、Draw Call 回数をできるだけ減らすことが重要となります。 一度の Draw Call で出来るだけ多くの描画を行えば効率が上がることがわかっているので、 画命令をまとめられるようにステート切り替えを減らす必要があります。 Geometry Instancing を使う、Texture Atlas を使う、などの手法が用いられるようになりました。
また Driver 負荷を下げると同時に、GPU の Pipeline フラッシュを避ける意味でも効果があります。
このような Driver 負荷に対して、API 自体を改良する動きが出てきました。 AMD の Mantle がその先陣を切っており、MS の DirectX12 が追従しています。
この両者とも、大きく削減されるのは CPU 負荷の方であり、GPU 自体に何らかの新機能が増えるわけではありません。 現在発売している GPU がすでに DirectX12 対応を謳っていることからも明らかです。
この点で、今までのメジャーアップデートとは目的が大きく異なっています。
API の刷新により、CPU 負荷を減らすことが最大の目的です。
アプリケーション負荷
- Thread による CPU core への分散
- SIMD による最適化
- GPU へのオフロード
CPU の特性上、ARM Cortex-A8 では SIMD 最適化が劇的な効果をあげていましたが、 それ以外では Multi core への Thread 化の方が効果を得やすいでしょう。
Mobile Device では Core 数や GPU 種類を一定に見積もりできないため、 ゲーム専用機と比べるとぎりぎりまでの最適化はできず、妥協は必要。 むしろ負荷よりも互換性の方が問題になりがちです。
GPU 負荷
GPU の構造によって特性がかなり異なってきます。 そのためどの手法を用いた方が良いのか必ずしも最適な方法があるとは限らず、GPU によっては意味が無かったり逆効果に働く可能性もあります。
演算精度
OpenGL ES のシェーダープログラムでは、precision 宣言が可能です。 用途に応じて演算精度を適切に変えることができます。
演算精度を下げる事による主なメリットは 2点。ただし GPU によって異なります。
- ALU の演算能力が増える
- レジスタの消費を抑えることでスレッド並列度が上がる
GPU のシェーダー演算能力には限りがあります。 ピーク性能は ALU の演算 Unit x 動作クロックで求まるので、この範囲でシェーダーの設計をしなければなりません。
一部の Mobile 向け GPU では、演算精度を下げることによりトータルの演算能力を増やすことができます。 CPU で言えば SSE 等 SIMD 命令と同じで、倍精度 (64bit fp) なら 128bit ALU で一度に 2演算ですが、 単精度 (32bit fp) なら 4演算が可能になります。
同じように GPU によっては、 highp (32bit fp) では 1演算ですが、mediump 宣言を行うと SIMD で同時に 2演算 (16bit fp) できるものがあります。 lowp (10bit fixed) も同様です。
演算精度を下げることで、シェーダーの実行パフォーマンスが向上する GPU としては PowerVR SGX, Tegra 2/3/4, Mali-400MP があります。
特に PowerVR SGX は ALU の演算能力がボトルネックになりやすく、精度指定によるシェーダーサイクルの削減が全体の描画速度に如実に現れます。 Tegra や Mali-400 の場合はテンポラリレジスタの削減による効果だと思われます。
ただし演算精度を下げることでクオリティも下がるので、シェーダー最適化時には用途に合わせた細かいチューニングが必要となります。
逆に Adreno は Desktop GPU と同じで常に highp (32bit fp) 演算が用いられており、 演算精度を指定しても変化がありません。
Alpha Test (discard) の是非
新しい GPU では、Alpha Test (discard) はパフォーマンスの低下を招く要因であると言われています。
例えば PowerVR などの Deferred Rendering (TBDR) との相性が悪くなります。 PowerVR は先に Depth Buffer を生成しておき、表面の必要なピクセルに対してのみ Pixel Shader (Fragment Shader) を走らせます。 ポリゴンが重なってもシェーダー負荷が一定以上上がらないので、効率よい描画ができることが特徴です。 ちょうど Desktop GPU で用いられる Deferred Lighting を、ハードウエアが自動的に行ってくれていることになります。 ところが、もし Shader に discard 命令が含まれているなら、Shader を実行してみないと正しい Depth Buffer を得ることができません。 よってポリゴンの重なりを効率良く取り除くことができなくなります。
古い GPU アーキテクチャにおいては必ずしも悪い面だけとは限りません。 discard (texkill) によって早いタイミングで Pixel Shader を終了させることにより、 後続のシェーダーの演算や無駄な Texture Fetch を省くことが可能になります。 Tegra 2/3/4 の ULP GeForce は G70 世代の古い GPU core なので、適切に discard (texkill) を使用した方がシェーダーの実行効率を上げることができると言われています。 例えば半透明描画時にテクスチャの Alpha 値が 0 とわかっているならば、カラーの演算結果は最終的に無駄になります。 カラーを出力しないで、テクスチャを読んだ直後に discard (texkill) してピクセルを捨てる方が都合が良いわけです。
ただし他の GPU との互換性や今後のことを考えると、使わない方が良いと思われます。