読者です 読者をやめる 読者になる 読者になる

syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

コンピュートシェーダーの性能

プログラミングTips DirectX Direct3D GPU

(これは2011-10-08に書いた故OCNブログの記事を加筆修正したものです)

DirectX 9世代のシェーダーモデル3.0までは、画面座標系でのポスト エフェクト処理(たとえばガウスぼかしやブルーム、トーン マッピング、SSAOなど)はレンダーターゲットを切り替えてピクセル シェーダーでテクスチャに描画することで行ない、さらに結果を次の入力テクスチャとして再利用することで実現してきました。DirectX 11やOpenGL 4.3/ES 3.1のコンピュート シェーダー (Compute Shader) は、物理シミュレーションのような本来のGPGPUらしい使い方のほかに、こういった従来からのポスト エフェクトを、より効率的に実現できる可能性を秘めています。

DirectX 10.x世代のシェーダーモデル4.x対応ハードウェア(ダウンレベル ハードウェア)でも、DirectX 11 APIを使えば、一応コンピュート シェーダーを動作させることはできます(OpenGL APIでは不可)。ですが、コンピュート シェーダー4.x(cs_4_x)およびピクセル シェーダー4.x(ps_4_x)にはかなり制約があって、例えばコンピュート シェーダー4から扱えるリソースタイプはStructuredBuffer、RWStructuredBuffer、ByteAddressBuffer、RWByteAddressBufferの4つだけです。ピクセル シェーダー4に至ってはStructuredBufferしか使えません。UAV (Unordered Access View) も1個だけなので、レジスタはu0しか使えません。
ひとつのリソースに対して複数のビューを割り当てることは可能で、構造化バッファとして作成したID3D11Bufferオブジェクトに対してUAVインターフェイスを作成し、そのUAVをコンピュート シェーダーのUAVスロットにバインドすることで、HLSLでRWStructuredBufferとして使えるようになります。一方、SRV (Shader Resource View) を作成してバインドすればStructuredBufferとして使えるようになります。ひとつのテクスチャリソースに対してSRVとRTV (Render Target View) を割り当てることができるのと同じです。

DirectX 11世代のシェーダーモデル5.0ではさらにRWTexture1D/RWTexture2D/RWTexture3Dがコンピュート シェーダー5(cs_5_0)およびピクセル シェーダー5(ps_5_0)から自由に使えます。ピクセル シェーダーでRW系リソースを使う機会はあまりないと思いますが、コンピュート シェーダーを本格的にポスト エフェクト用途に用いるならば、RWTexture系のリソースが使えるかどうかは非常に重要になってきます。
GDC 2009にて、AMDは効率的なポスト エフェクトをコンピュート シェーダーで実装するコツを紹介しています。

Intelも開発者向けWebサイトにて、ポスト エフェクトにおけるコンピュート シェーダーの優位性を紹介しています。
http://software.intel.com/en-us/articles/compute-shader-hdr-and-bloom

たとえばコンピュート シェーダーを使ったブルーム処理(ぼかし+加算合成)の場合、下記のような流れになります。

(1) まずピクセル シェーダーを使って、シーンをレンダリングした元画像(任意サイズ、1280x768のTexture2D/Texture2DMSとか)を一時テクスチャ(128x128のTexture2Dとか)へ高輝度成分のみ抽出しながらダウンサンプル。

(2) コンピュート シェーダーで、ダウンサンプル テクスチャ@t0をフェッチし、いったんラインごとのグループ共有メモリに書き込み、その後でRWTexture2D#0@u0へ水平方向ガウスぼかしカーネル適用結果を書き込む(このとき、後段の垂直ぼかしの際に高速読み込みできるよう、出力位置インデックスを転置しておく)。

(3) コンピュート シェーダーで、Texture2D#0@t0をフェッチし、いったんラインごとのグループ共有メモリに書き込み、その後でRWTexture2D#1@u0へ垂直方向ガウスぼかしカーネル適用結果を書き込む(このとき、出力位置インデックスを再び転置することでキャンセルされて縦横が元に戻る)。

(4) Texture2D#1@t0をピクセル シェーダーから読み取り&サンプリングしながら、元のシーンに対して加算合成描画する。

基本的な流れはピクセル シェーダーのみを使ったバージョンと大差なく、最も大きな違いはRTVの代わりにUAVを使うことなのですが、ピクセル シェーダーの仕事をコンピュート シェーダーに一部委ねる形になります。そしてコンピュート シェーダーにおいて高速化の要となるのがグループ共有メモリです。グループ共有メモリは容量が小さいものの、各コンピュート スレッド グループ内で共有できるキャッシュ メモリで、テクスチャや構造化バッファといった、グローバル メモリへの直接アクセスよりもはるかに高速に読み書きできます。CPUのキャッシュ メモリはプログラムで直接制御することはありませんが、このグループ共有メモリは我々プログラマーが直接読み書きを指示制御することができます。特に同じ読み込みが何度も必要となるようなカーネル サイズの大きいフィルターを適用するときに、絶大な効果を発揮するでしょう(ピクセル シェーダーの場合、1ピクセルに対してフィルターを適用するとき、都度テクスチャから何度も読み込みが必要でした)。なお、場合によっては(1)もコンピュート シェーダーで実装できるかもしれません(トーン マッピングと併せて行ないます)。
ときどきコンピュート シェーダー不要論なぞを標榜する人がいますが、GPGPUをやったことのある人間からすればまったくとんでもない話です。このコンピュート シェーダーにおけるグループ共有メモリというのは、他のシェーダーステージにはない決定的な機能で、今後のハイエンド グラフィックス プログラミングではコンピュート シェーダーをいかに使いこなせるかが最重要となってくるはずです。ただ、グループ共有メモリを活用したコンピュート シェーダープログラムは非同期並列プログラミングに関する知識やGPUハードウェア特性に関する知識も必要となるため、ピクセル シェーダーとは異なる次元の難しさ・取っつきにくさがあります。コンピュート シェーダー不要論を唱える人というのはただの無知か、もしくは食わず嫌いのどちらかでしょう。

ちなみに、DirectX SDKのサンプル、HDRToneMappingCS11はコンピュート シェーダー版のほうがむしろピクセルシェーダー版よりも実測パフォーマンスが落ちるんですが、これは解説をよく読むと、ダウンレベル ハードウェア(cs_4_0)用にRWTexture2DでなくRWStructuredBufferを使っていて、2Dテクスチャへの変換という余計な処理が加わっていることなどが原因の模様です。シェーダーモデル5.0専用(cs_5_0)に書き直せばピクセル シェーダーよりも高速化する可能性があります。
また、実装コードをよく読んでみると、(コンピュート シェーダーとピクセル シェーダーの比較デモであるにもかかわらず)コンピュート シェーダーのほうがだいぶ不利なコードになっているようです。ソースコードがだいぶ汚く、C++側とHLSL側とで対応する数値を見つけるのがひどく煩雑で異様に解析しづらいサンプルなのですが、おおざっぱに説明すると、

・コンピュート シェーダー:

 8x8=64サイズのブロックでタイルベース2D→1D並列リダクションを実行したのち、128サイズのブロックで1D→1pix並列リダクションを実行。cs_4_0なのでリダクションは構造化バッファで実行している。

・ピクセル シェーダー:

 2x2カーネルのリダクションののち、3x3カーネルのリダクションを複数回実行して1pix化。

となっていて、コンピュート シェーダーのほうは構造化バッファを経由するオーバーヘッドだけでなく、ブロック サイズ(ローカル スレッド グループ サイズ)が小さめなのが足かせになっている模様。NVIDIA GPUには32スレッドを1単位とするWarp(ウォープ)という概念があり、またAMD GPUには64スレッドを1単位とするWavefrontという概念があるのですが、特にコンピュート シェーダーのブロックサイズが小さいとプロセッサの稼働率を最大化することができずにパフォーマンスが落ちてしまうとのことです。1ブロックあたりに割り当て可能な共有メモリのサイズや、スレッドごとのレジスタ数には上限があるので、それとの兼ね合いにもなりますが、Fermi世代では少なくともブロックサイズが128, 256, 512, 1024のいずれかでないと効率が最大になりません。詳しくはNVIDIA CUDA Occupancy Calculatorを参照のこと。NVIDIA環境でのコンピュート シェーダーはCUDAアーキテクチャにて実行されるため、CUDAコードを最適化するための手法がコンピュート シェーダーにもかなり通用するので、NVIDIA環境でコンピュート シェーダーを最適化する場合はCUDAを勉強しておくと必ず役に立つと思います*1

なお、コンピュート シェーダーのGPGPU用途としては、例えばこれまでピクセル シェーダーで疑似的に実装していた水面の波紋シミュレーションなどを、コンピュート シェーダーで実装するのもよいかもしれません。これに関してはPC版ロストプラネット2でWave Particlesを実装するためにコンピュート シェーダーが活用されているそうです。CEDEC 2010レポートのページも実践的で参考になります。境界面で反射する波(自由端/固定端)や、キャラクターの移動に対してインタラクティブに追従するドップラー効果というのは個人的にも実装してみたい技術です。

*1:NVIDIA環境でOpenCLOpenGLコンピュート シェーダーを使う場合も、CUDAを勉強しておくとよいです。