日別アーカイブ: 2007年10月1日

Direct3D 10 Shader4.0 消えた abs と最適化

テクスチャから読み込んだピクセルの値が 0.1 付近であることを
判定しようとして、HLSL で当初こんなコードを書いていました。
テクスチャのピクセルサイズが不明なので、± 0.05 くらいの
誤差を許容しています。

int TestA( float4 color )
{
    return  color.x > 0.05f && color.x < 0.15f;
}

あまりにそのまんまなので、もう少しちゃんと書こうとして次の
ように修正してみました。

int TestB( float4 color )
{
    return  abs( color.x - 0.1f ) < 0.05f;
}

どちらが複雑度が高いか比較するためにアセンブラで確認してみます。
結果は上の TestA() 3命令で、下の TestB() が 2命令に展開されています。

// TestA
ge r1.x, r0.x, l(0.050000)
ge r1.y, l(0.150000), r0.x
and r1.x, r1.x, r1.y

// TestB
add r1.x, r0.x, l(-0.100000)
lt r1.x, |r1.x|, l(0.050000)

見てわかるとおり abs 命令がありません。lt 命令のソースオペランド
に直接絶対値指定らしき修飾子 |~| が記述されています。

Shader3.0 までは、abs は独立した命令 'abs' だったので便利に
なっています。良く考えたらもともと符号反転は出来たので、符号を
落とすだけの abs も簡単なのかもしれません。

ただし共通バイトコードでは単なる修飾子でも、ドライバレベル以降の
ネイティブコードでは独立した命令として実行される可能性があります。
NVIDIA の OpenGL 拡張命令から D3D10 Shader4.0 相当の ASM Shader
を調べてみるとしっかり ABS 命令が存在していました。

実際に比べてみます。もともと動作しているシェーダーに abs を挿入
してコンパイルし、消費する命令 slot 数が変化しないことを確認します。
比較しやすいように 300回ほどループさせて比べます。

// abs 無し
    loop 
      ige r2.x, r1.w, l(300)
      breakc_nz r2.x
      mad r1.xyz, v0.xyzx, v1.xxxx, r1.xyzx
      iadd r1.w, r1.w, l(1)
    endloop 

// abs あり
    loop 
      ige r2.x, r1.w, l(300)
      breakc_nz r2.x
      mad r1.xyz, |v0.xyzx|, |v1.xxxx|, r1.xyzx
      iadd r1.w, r1.w, l(1)
    endloop 

GeForce8800GTS で走らせたところ速度差が出ました。

10000~10200 (usec)   abs無し
14500~15400 (usec)   absあり

GeForce8800(G80) では実際には abs は個別の命令となっていて、
それぞれの絶対値演算で別の実行サイクルを消費しているように見えます。
つまり

 mad r1.xyz, |v0.xyzx|, |v1.xxxx|, r1.xyzx

は本当は 3命令相当で、さらに内部で追加の temp レジスタを消費
している可能性もあります。

このことから、Direct3D 上のバイトコードで命令 slot 数やレジスタ
数をみても、実行速度や最適化の目安に過ぎないということがわかります。
また HLSL コンパイラの段階では abs をコストフリーと考えて
最適化している可能性もあります。

将来ぎりぎりの最適化を行うようになったら、この辺は要注意ですね。

さて、最初に戻って TestA と TestB の比較ですが、ge ×2 + and の
TestA よりも、abs を使った TestB の方がずっと高速でした。
数値そのものは他の処理も含んでいるのであまり意味を持たないのですが、
差が生じていることはわかります。

26000 (usec)   TestA
15000 (usec)   TestB

もしかしたら単なる命令差よりも、文脈上 B の方が消費レジスタが1つ
少ないことが原因かもしれません。

例えば

E= A*B*C*D

という演算を行う場合、一般的な CPU や Shader1.0 世代の GPU では

(1) R1= A*B
(2) R1= R1*C
(3) R1= R1*D

よりも

(4) R1= A*B
(5) R2= C*D
(6) R1= R1*R2

の方が高速です。これは最適化のテクニックとしても良く用いられます。
その理由は (1)~(3) の演算にはすべて依存関係があり、前の演算結果が
が出るまでパイプラインがストールするからです。

(4) と (5) は依存関係が全く無いので、完全に並列演算が可能となります。
アウトオブオーダーなら C,D の準備が出来次第、(4) よりも (5) を
先に実行するかもしれません。

ところが今の GPU は逆であり、パイプラインストールは完全に他の
スレッドで埋めてしまいます。よって (4)~(6) は余計な R2 を消費する
分だけ逆にスレッド並列化を阻害し、(1)~(3) よりも低速になってしまう
わけです。

TestA と TestB の関係も同じで、TestA の方が最初の2命令に依存関係が
無いので、今までの感覚で見ているとついこちら方が良いコードに見えて
しまいます。