高度な検索
Developer Connection
Member Login ログイン | ご入会 ADC連絡先

Technical Note TN2093
OpenGL Performance Optimization : The Basics

はじめに

OpenGL コードの最適化は、OpenGL ベースのアプリケーションの開発においてますます重要な作業になっています。この記事は、アプリケーションのパフォーマンス向上を目指している OpenGL 開発者を対象としています。開発者は、ここに提示した情報を十分に理解し最大限活用するためには、OpenGLプログラミングに関する基礎知識を持ち、Mac OS X 上の OpenGL に精通している必要があります。

OpenGL アプリケーションのパフォーマンスチューニングを開始するためにコードに取り組む前に、OpenGL 最適化の基礎を確認して、OpenGL アプリケーションのパフォーマンスの強化に体系的に取り組む方法を築き上げるのが最善です。最初に行うべきことの 1 つは、(できればウインドウの中で)アプリケーションを起動し、ターミナルウインドウの中で「top」コマンドを実行することです。アプリケーションによって使用されている CPU 時間が表示されるため、これがほとんどすべてのパフォーマンス分析の出発点となります。この情報は CHUD ツール「Shark」から得ることもできます。ただし、チューニングプロセスのこの段階で Shark を使用するのはやり過ぎかもしれません。ここでの目的は、OpenGL の以降のチューニングのベースラインとなる値を得ることです。

続いて、OpenGL Profiler を通してアプリケーションを実行し、アプリケーションが OpenGL に関してどこでどのように時間を費やしているのかを示す統計情報を収集します。この 2 つの量が分かれば、OpenGL に費やされた実際の時間と、使用された CPU 時間を組み合わせて、パフォーマンスの概算値を得ることができます。これらの概算値によって、開発者はアプリケーションがどのくらいの CPU 時間を使用し、そのうちどの程度が実際に OpenGL に費やされているかを初めて確認することができます。

先頭に戻る

パフォーマンスチューニングのロードマップ

  • 「top」(または Shark)と並行して OpenGL アプリケーション(ウインドウ表示)を実行する

    これにより、アプリケーションによって費やされる CPU 時間について、ベースラインとなるパフォーマンスの数字が得られます。「top」の画面表示例を下に示します。対象アプリケーション(NSGLWindow)は強調表示されています。

  • ベースライン CPU 使用率を調べる

    上図の時間に注目してください。NSGLWindow アプリケーションは現在、使用可能な CPU 時間の 19.5% を使用しています。この値が、アプリケーションのパフォーマンスの判断に使用するベースライン値です。

  • OpenGL Profiler を通じて OpenGL アプリケーションを実行し、関数トレースと統計情報を収集する

    次の OpenGL Profiler のダイアログに示すように、新しいプロファイルを作成するように設定します。

    次の OpenGL Profiler のダイアログに示すように、アプリケーションのトレースおよび統計情報を収集するように設定します。

    「Launch(起動)」ボタンを押すと、対象アプリケーションが開始され、Profiler は指定されたデータの収集を静かに開始します。アプリケーションの動作中、特に Profiler が関数トレースを収集しているときに、一時的な中断や誤動作が見られる場合があります。これは正常であり、パフォーマンス統計情報に大きな影響はありません。原因は、Profiler が関数トレースのリストのために大量のデータを収集して書き出していることです。

  • Profiler 統計データを分析、OpenGL で費やされたアプリケーション時間の割合とその時間が費やされた場所を特定する

    次の図は、NSGLWindow の例の Profiler 統計情報を示します。リストは、OpenGL 時間を最も費やしているものから降順にソートされています。この図は、OpenGL の時間の大半が、画面へのピクセルデータのプッシュに費やされたことを明確に示します(CGLFlushDrawable() )。

    上図の左下隅には、「Estimated % time in OpenGL: 14.25%」というラベルの付いた小さなデータがあります。分析のこの時点では、この数字が最大の関心事です。この数字が大きいほど、アプリケーションが OpenGL に費やしている時間が長いということであり、OpenGL を最適化してアプリケーション性能を向上できる可能性が高くなります。

  • Profiler 関数トレースを分析、重複する関数呼び出しおよび冗長または不要な状態変更を探す

    次の図は関数トレース自体のものです。これは完全な関数トレースの部分的なリストにすぎず、1 ページあたり 50 行だけ表示されます。このようなページが何千ページもあり、Profiler が関数トレースを集め続けると何百万ページにもなる場合もあります。このリストは、ページ下部にある矢印を使用してスクロールできます。外側の左右の矢印はそれぞれ、先頭ページと最終ページにジャンプします。また、内側の左右の矢印はそれぞれ、前後に 1 ページ単位でスクロールします。

    トレースは、重複する関数呼び出しまたは冗長な状態変更の検出に非常に役立つ場合があります。トレースを調べるときには、同一または類似のデータを使用する隣り合わせの関数呼び出しを探します。これらは、関数呼び出しのオーバヘッドを少し減らすために、コードの中で一般的に最適化できる領域です。冗長な状態変更を探すときによく見られる重複には、glTexParameter*()、glPixelStore*()、glEnable()、glDisable() などの関数があります。多くの場合、これらの関数は設定ルーチンまたは状態変更ルーチンから一度呼び出せば、後は必要に応じて呼び出すだけで済みます。一般的に、状態変更は可能な限りレンダリングループ(関数トレースには、同じシーケンスの状態変更と描画として何度も繰り返し登場する)の外に置き、別のルーチンを使用して必要に応じて状態を変更するのがよい方法です。

  • OpenGL のオーバーヘッドがゼロになった場合のパフォーマンス上の最大のメリットは何かを判断する

    この場合には、実際のパフォーマンスデータを使用して、OpenGL のパフォーマンス向上の理想的なレベルを決定することができます。上記で生成された次の数字を使用すると、後述の方程式からいくつかの興味深い結果が得られます。

    アプリケーション合計時間(「top」から) = 19.5%

    OpenGL における合計時間(「Profiler」から) = 14.25%

    最初に見たときは、OpenGL パフォーマンスを向上させれば、アプリケーションのパフォーマンスが 15% 近く向上し、アプリケーションの合計時間が 15% 削減されると考える人もいるかもしれません。残念ながら、事実はそれほど単純ではありません。以下のセクションで説明するように、ここでの問題は、これら 2 つの数字が互いにどのように関連するかを理解することです。

先頭に戻る

OpenGL パフォーマンスの理解

おそらく、OpenGL パフォーマンスの最も重要な側面は、それがアプリケーションのパフォーマンス全体とどのように関連するかということです。NSGLWindow の例の上記データを見ると、使用可能な CPU 時間 の 19.5% がアプリケーションによって費やされています。この 19.5% のうち、14.25% が OpenGL に費やされており、残りがアプリケーション自体によって使用されています。以下の方程式は、OpenGL パフォーマンスとアプリケーションパフォーマンスの関係を示します。

OpenGL パフォーマンス向上の全体 =(費やした CPU 時間の合計)*(OpenGL に費やした時間のパーセンテージ)

この方程式に生成されたデータを当てはめると、次の結果が得られます。

OpenGL パフォーマンス向上の全体 = (19.5) * (14.25%) = 2.77875%

注意:計算機に入力する実際の数値は、上記とは異なります。実際の数字は次のとおりです。

19.5 * (0.1425) = 2.77875

実際の値に求めるために、パーセンテージのパーセンテージを算出します。

以上を考慮すると、OpenGL がまったくの「no op」になったとしても(処理時間 0)、アプリケーションのパフォーマンス向上は 2.78% にとどまります。つまり、アプリケーションが 60 フレーム/秒で動作していた場合は、そのパフォーマンスは次のようになります。

新しいフレームレート = 前の FPS *(1 + パフォーマンス向上のパーセンテージ)= 60fps * (1.0278) = 61.67fps

アプリケーションは、OpenGL の処理を 15% から 0% まで減少させても、2 フレーム/秒を若干下回る向上が得られるくらいです。これは、OpenGL のパフォーマンスとアプリケーションのパフォーマンスの関係が直線的ではなく、OpenGL に費やされる時間の削減だけでは、アプリケーションのパフォーマンスに大きく寄与しない場合があることを明確に示します。

注意:OpenGL アプリケーションの CPU 使用率を実際にゼロにして、少しでも何か有用な処理を行えると考えるのは実際的ではありません。OpenGL のオーバヘッドをゼロまで削減するという考えは、あくまでも OpenGL 最適化の背後にある考え方を示すための方便に過ぎません。

次の図は、上に提示した情報をグラフとして表したものです。これは OpenGL のパフォーマンスとアプリケーション全体のパフォーマンスの関係に加えて、パフォーマンスデータの実際の値を知る方法を示します。

上の図に示したシナリオは、先の例よりは多く実際の世界で見られる可能性があります。OpenGL の処理がアプリケーションが使用する CPU 時間の約 25% を使用しています。実際に使用された時間は示してありませんが、考え方を示すためには必ずしも必要ではないためです。ここ示しているのは、アプリケーションが実際に使用する時間にかかわらず、OpenGL がその時間の比較的わずかな割合(25%)のみを使用し、アプリケーションが残り(75%)を使用するという点です。

先頭に戻る

Profiler、Driver Monitor、および CHUD ツールの使用

前のセクションでは、OpenGL Profiler を使用して OpenGL アプリケーションのパフォーマンスデータを収集するためのヒントと説明を提供しました。Profiler を使用すると、開発者は OpenGL に費やされる時間、その時間を費やす関数、および分析対象アプリケーションの関数呼び出しトレースを確認することができます。OpenGL Profiler には、前述したものだけでなく、さらに多くの機能と関数が含まれています。OpenGL Profiler のより詳細な説明については、「OpenGL Profiler」の Web ページを参照してください。

OpenGL アプリケーションのパフォーマンスチューニングでは、Mac OS X Developer Tools に含まれている上記の 3 つのツールが最も重要です。これらは、OpenGL アプリケーションに見られる一般的なパフォーマンス問題の多くを突き止め、明らかにすることができます。この記事では、これらのツールに関する大量の情報を再掲するのではなく、代わりに該当するツールの資料へのリンクのリストを後に掲載しています。

Profiler を使用するときには、留意すべきことが 2、3 あります。以下は、Profiler を使い始めるときの注意事項を示す短いリストです。

  • 統計情報を収集して、どこで OpenGL に時間が費やされているかを確認する

  • 関数トレースを収集して、重複する関数呼び出しと冗長な状態変更を探す

  • 関数統計情報で glFinish() コマンドを探し、可能であればコードから削除する

  • 頂点サブミットコマンドを確認する。頂点がどのように OpenGL にサブミットされているかを調べる

「OpenGL Driver Monitor」 には最初圧倒されるかもしれません。したがって、表示されるデータをよりよく理解できるように、「OpenGL Driver Monitor Decoder Ring」 に目を通しておきましょう。当該記事では、Driver Monitor のさまざまな側面と、アプリケーション内で検査できる重要な統計情報についてある程度詳しく説明しています。Driver Monitor 自体の詳細については、「OpenGL Driver Monitor」 の Web ページを参照してください。

OpenGL アプリケーションと一緒に Driver Monitor を実行している様子を示すこの画面では、ドライバのほぼすべてのパラメータと状態を表示・分析することができます。この例では、Driver Monitor によって現在追跡されている項目は、bufferSwapCount、clientGLWaitTimehardwareSubmitWaitTimehardwareWaitTime の 4 つです。最初の項目は比較的簡単です。bufferSwapCount はドライバが実行したバッファスワップの総数です。2 番目の clientGLWaitTime は、クライアントの OpenGL ドライバがハードウェアタイムスタンプの到着を待機している間、CPU を停止させている時間です。これは通常、テクスチャの更新または glFence() コマンドの完了を待っている間に起こります。3 番目のパラメータ hardwareSubmitWaitTime は、新しい一群の OpenGL コマンドをサブミットできるまで待機している間、CPU を停止させている時間を示します。GPU が以前にサブミットされたコマンドバッファを処理するのを待機している間の、CPU が浪費している時間を知ることができるので、これは特に重要な機能です。最後のパラメータ hardwareWaitTime は、GPU を待機している間に CPU が停止している時間を示す包括的なインジケータです。このパラメータには、hardwareSubmitWaitTime などのハードウェア待機状態に関するパラメータも含みます。

CHUD ツールは、.Mac OS X においてコードを最適化する開発者を支援するために設計された一式のツールです。OpenGL アプリケーションに関しては、これらのツールの中で Shark がおそらく最も役に立ちます。Shark は、開発者がコードレベルでパフォーマンス問題の場所を判断できるパフォーマンス分析ツールです。Shark の詳細については、「Using Shark」を参照してください。

先頭に戻る

重複する関数呼び出しおよび冗長な状態変更の検出と排除

OpenGL パフォーマンス問題の主要な原因の 1 つは重複する関数呼び出しです。この問題の携帯はさまざまで、冗長な状態設定、単一フレームにおける多重フラッシュや多重スワップなどが含まれます。たとえば、glEnable(GL_LIGHTING) などの呼び出しでライティングを有効にしたり、glEnable(GL_TEXTURE_2D) でテクスチャを有効にする場合、これらの呼び出しは、有効にするために一度、無効にするためにもう一度行えば済むはずです。よくあるのは、アプリケーションが描画ループを通過するたびにテクスチャまたはライティングを有効にするというシナリオです。一般的に言って、このような状態変更をアプリケーション全体で使用する場合には変更を一度だけ行えばよく、専用の設定ルーチンで行うべきです。しかし、特定の視覚効果または描画操作を達成するために、テクスチャまたはライティングをオフにしてから、再度オンにしなければならない場合があります(テクスチャが付加されたポリゴンの周りにワイヤーフレームを描画する場合など)。この場合、必要な場合にのみ状態を変更する独立のルーチンを用意し、OpenGL 自体ではなくアプリケーションレベルで実行するべきです。

OpenGL がどのような整合性検査も冗長状態セット検査も実行しない点を理解することが重要です。たとえば、前述のように、glEnable(GL_LIGHTING) などの呼び出しを実行して、その後でこれと同じ呼び出しをしても、OpenGL は状態が実際に変化したことを確認しません。OpenGL は渡されたパラメータの状態値が現在の値とまったく同じでも、単純にその値を更新するだけです。これは OpenGL 仕様に定められている設計であり、実装固有ではありません。これらの検査の実行に必要な追加コードは、開発者の役には立ちますが、このようなことをしていなかったアプリケーションでも必然的にパフォーマンスの問題を引き起こします。

OpenGL における状態変更は高くつく傾向があるため、独立の初期化ルーチンまたは設定ルーチンに切り出しておくべきです。これらの呼び出しを描画ループ、あるいは描画ループで実行される関数に配置すると、不要な状態変更によって OpenGL のパフォーマンスが低下します。OpenGL はパフォーマンス志向であるため、渡される状態変更に関して条件またはエラーの検査を実行しません。したがって、これらの呼び出しは、データの変更と同じ分だけ、冗長なエントリのために余分なコストがかかることになります。

一例として、NSGLWindow の例を変更して、描画ルーチンの 1 つに冗長な glFlush() 呼び出しを含めました(drawCube() 関数に、glEnd() 関数の各呼び出しの直後に glFlush() を追加して、合計 2 つの余分な呼び出しをコードに追加しました)。その後、変更した実行ファイルを OpenGL Profiler を通じて実行し、関数トレースと統計情報を収集しました。そのデータの結果は次のとおりです。

一見すると、アプリケーションの描画ループがオーバードライブされているように見えます(CGLFlushDrawable() で費やされた大量の時間は、たいていこのような事象を示します)。たとえそうだとしても、glBegin() と glDeleteTextures() はどちらも、重複させたコマンドよりも OpenGL で長い時間を費やしています。

トレース内の glFlush() 呼び出しの回数に注目してください。このコマンドは、明らかにレンダリングループで、すべての描画コードのブロックの後で発行されています。これらは、パフォーマンスの点から見て冗長かつ不経済です。

冗長な glFlush() 呼び出しを削除すると、OpenGL で費やされる時間が著しく減少します。特に CGLFlushDrawable() では、費やされる時間が 10% 以上減少します。glFlush() は 5% から 3% 未満に減少し、OpenGL で費やされる時間全体は 2% 弱減少します。これらの向上は劇的というほどではありませんが、これが比較的単純なアプリケーションで、グラフィックスパイプラインの限界を押し広げるようなものではないことに留意してください。より現実的な設定では、これよりも大きな時間の割合を占めるかもしれません。

興味深いのは、最後に示したスクリーンショットのパフォーマンス統計情報とその前の統計情報によると、glBegin() が 5% 増加したことです。理由は、残りの関数呼び出しに対して、glBegin() が以前よりも大きな時間の割合を占めているからであり、費やしている時間が増えているわけではありません。

先頭に戻る

glFlush() および glFinish() の効果的な使用

この 2 つのコマンドは、本質的には同じことを実行するのに使用されます。つまり、キューに入れられたすべての OpenGL コマンドを、実行のためにハードウェアにサブミットします。両者の主要な違いは、glFinish() はサブミットしたコマンドがすべてハードウェアで実行されるまでブロックするのに対し、glFlush はすべてのコマンドがサブミットされるまで待機するという点です。このことだけでも、glFinish()glFlush() よりもはるかに重大な問題を引き起こす可能性があることは明白です。

この 2 つの関数呼び出しを中心とする問題は通常、簡単に突き止めることができます。これらのコマンドは使い方を誤まると、停止や速度低下を引き起こす場合があり、必然的にアプリケーションのパフォーマンスが貧弱になります。これは通常、つっかえつっかえの鈍い応答、および高い CPU 使用率として現れます。glFlush() または glFinish() に原因がある場合は、OpenGL Profiler からの統計レポートをざっと見るだけでどこに問題があるか分かるはずです。

ご想像のとおり、glFlush() がパフォーマンスに与える影響は glFinish() よりもはるかに小さいものです。パフォーマンスを向上させるには、絶対に必要であると考えられる場合を除き、glFinish() コマンドを排除するべきです。glFlush() コマンドは、効率的な方法で使用する限りは使用することができます。たとえば、描画ループの最後に描画の更新を強制するために glFlush() を使用できますが、バッファスワップコマンド(aglSwapBuffers() など。これ自身、glFlush() が暗黙に含まれています)を呼び出す直前にこれを実行するべきではありません。この 2 つのコマンドの詳細については、Q&A の glFlush() vs glFinish() を参照してください。当該記事では、glFlush() および glFinish とそのパフォーマンスに対する影響について明確に説明しています。

パフォーマンスに対する glFlush() の影響は前のセクションで説明しましたが、それと同じ視点で glFlush()glFinish() の違いを理解することも重要です。次の例では、同じ glFlush() コマンドを glFinish() コマンドに置き換えた場合に何が起こるかを示します。

最も時間を消費しているのが glFinish() であり、CGLFlushDrawable() よりも多いことに注目してください。OpenGL で費やされた合計時間は、以前に glFlush() コマンドに関して記録された 24% から 29% 以上に達しています。OpenGL で費やされた時間の 61% が glFinish() の呼び出しで費やされました。Driver Monitor で見たとしたら、このデータは、glFinish() の呼び出し時点までにハードウェアに対してサブミットされたすべてのコマンドの実行の終了まで待機する間に費やされた時間が大きなパーセンテージを占めていることを示すでしょう。

先頭に戻る

(レンダリングタイマを使用して)グラフィックスパイプラインをオーバードライブしない

もう 1 つの一般的なパフォーマンス問題は、アプリケーションが描画ループをオーバードライブしようとすることです。これは通常、短い間隔で立て続けに作動するタイマを使用し、作動のたびに描画ループを呼び出すことで行われます。典型的には、タイマ間隔が例外的に小さい値(たとえば、毎秒 1000 回の実行のために 0.001 など)に設定されています。しかし、その効果はしばしば、期待とは正反対です。CPU 時間は通常の 2 倍または 3 倍(時にはそれ以上)消費され、アプリケーションのパフォーマンスが大幅に低下します。このような場合は、システムに描画の制御を任せるか(たとえば、Cocoa では -setNeedsDisplay: を使用)、タイマ間隔を大幅に延長するのが最善です。タイマ間隔を延長する場合は、0.01(毎秒 100 回の呼び出し)などの値から始め、そこからタイマ間隔を上下に調整し、目標のフレームレートを見つけるのが最善です。

この問題の詳細と短いコード例については、「NSTimers and Rendering Loops」 を参照してください。当該記事に示されているコードは、NSTimer と適度な作動間隔で駆動される描画ループの適切なアーキテクチャに関する明確な実例を提供します。

先頭に戻る

VSYNCH の理解

アプリケーションはフレーム分裂の問題を排除するために、垂直リフレッシュ(VBL、垂直ブランクまたは vsynch)と同期しています。フレーム分裂とは、画面上でのフレームのレンダリングが完了する前に、次のフレームの一部がフレームバッファ内の前のフレームのデータを上書きしてしまう状況を指します。表示上の影響としては、(状況によって程度の差はありますが)新しいフレームの半分と前のフレームの残りが見えるというようなものになります。垂直リフレッシュに同期すると、垂直リトレース時(電子銃が開始点に戻るとき)にフレームを描画することでこの問題は解決します。これによって、画面がリフレッシュされるたびに、フレームが 1 つだけ描画されるようになります。

ただし、これにはいくつかの注意すべき点があります。まず、リフレッシュはモニタの現行リフレッシュレートの整数の因数でのみ行われます(60Hz、30Hz、15Hz など)。ここでの問題は、OpenGLが次の垂直リトレースを待つ間ブロックされるため、他の描画操作に使える時間が無駄になるということです。

注意:液晶ディスプレイは、従来の意味での「垂直リトレース」がなく、一般的に 60Hz のリフレッシュレートに「固定」されています。

先頭に戻る

フレームバッファからピクセルを読み取る

glReadPixels() は、まさにその性質からして不経済な関数呼び出しです。したがって、glReadPixels() を使用するときには、可能な限り最も効果的で効率的な方法で行うように注意する必要があります。この glReadPixels() 呼び出しにより、同期ポイントがコマンドストリームに置かれます。この同期ポイントは、CPU と GPU を強制的に同期するため、レンダリングパイプラインを停止させるという影響を及ぼすことがあります。これが起こると、CPU または GPU が他方を待機する間、パフォーマンスが悪影響を受けるのは間違いありません。

glReadPixels() に代わる手段として、非同期テクスチャフェッチを使用することもできます。基本的には、非同期テクスチャフェッチは、GL_APPLE_CLIENT_STORAGEGL_APPLE_TEXTURE_RANGE を設定したテクスチャアップロードと同じパイプラインを使用しますが、アップロードではなくダウンロードを実行するために、実行の順序が逆になります。これが実行するのはテクスチャデータを VRAM から AGP テクスチャに DMA することです。以降、アプリケーションから直接データにアクセスできるようになります。ストレージヒント GL_APPLE_CLIENT_STORAGE_HINT_SHARED を使用すれば、テクスチャのドライバコピーが排除され、VRAM にのみ存在するようになるので、スループットが向上します。データ転送を開始するには、glCopyTexSubImage2d() を呼び出し、その直後に、glFlush() を呼び出します。これでテクスチャデータが AGP テクスチャに入れられ、glGetTexImage() の呼び出しで、このテクスチャがシステムメモリに転送されます。追加処理のために中間の時間を考慮に入れて、できるだけ最後の瞬間まで待ったうえで、AGP からシステムメモリへの転送を実行するのが最善です。

先頭に戻る

まとめ

結論として、以上の情報は Mac OS X 上で高速かつ最適化された OpenGL アプリケーションを開発するための確かな基盤を提供するはずです。ここで覚えておくべき重要なことは、これが実際には、いわば「氷山の一角」であり、アプリケーションのパフォーマンスをさらに向上させるために利用できる方法やテクニックが他にも数多くあるということです。留意すべきもう 1 つの点は、すべてのアプリケーションが同じ方法でグラフィックパイプラインを駆動するわけではないので、アプリケーションの設計方法やレンダリングの処理方法に応じて、異なる最適化手法が必要になるという点です。

先頭に戻る

参考資料

OpenGL Driver Monitor

OpenGL Driver Monitor Decoder Ring

OpenGL Profiler

CHUD Tools

Using Shark

glFlush() vs glFinish()

NSTimers and Rendering Loops

また、過去の WWDC セッションにおける OpenGL のプレゼンテーションも、OpenGL のパフォーマンスに関する非常に有益な参考資料です。カンファレンスに出席したすべての開発者は、これらを配布 DVD から利用できます。

先頭に戻る

ドキュメントの改訂履歴

日付メモ
2004-12-01初版

掲載日: 2004-12-01