日別アーカイブ: 2007年8月19日

Direct3D 10 Shader4.0 ループと最適化

Direct3D 10 の Shader4.0 は、実行可能な命令スロットも多いし
整数型も整数演算も扱えるし、HLSL を使うと Shader であることを
忘れそうになります。
条件分岐やループ命令等もそのまま記述し、当たり前のように実行
できるようになりました。

例えばまったく意味のない内容ですが、VertexShader の最後に
わざとこんなコードを書いてみます。

Out.Normal= 0;
for( int i= 0 ; i< 200 ; i++ ){
	Out.Normal+= In.Normal;
	Out.Normal+= wpos;
}

描画するデータは次のとおり。

・Teapot (Maxのプリミティブ)
・ポリゴン数 57600
・頂点数 29646

Pixel の影響をできるだけ受けないように、PixelShader は固定の
定数を return するだけとし、描画面積も点に近い状態でテストします。

実行するとさすがに時間がかかって、GPU 時間で 1410 usec ほど
消費しました。(GeForce8800GTS 640)
コンパイルされたコードはこんな感じです。命令コードに loop 命令が
あって、本当に Shader 内でループ実行されていることがわかります。

    vs_4_0
    dcl_input v0.xyz
    dcl_input v1.xyz
    dcl_output_siv o0.xyzw , position
    dcl_output o1.xyz
    dcl_constantbuffer cb0[8], immediateIndexed
    dcl_constantbuffer cb1[3], immediateIndexed
    dcl_temps 3
    mov r0.xyz, v0.xyzx
    mov r0.w, l(1.000000)
    dp4 r1.x, cb1[0].xyzw, r0.xyzw
    dp4 r1.y, cb1[1].xyzw, r0.xyzw
    dp4 r1.z, cb1[2].xyzw, r0.xyzw
    mul r0.xyzw, r1.yyyy, cb0[5].xyzw
    mad r0.xyzw, cb0[4].xyzw, r1.xxxx, r0.xyzw
    mad r0.xyzw, cb0[6].xyzw, r1.zzzz, r0.xyzw
    add o0.xyzw, r0.xyzw, cb0[7].xyzw
    mov r0.xyzw, l(0,0,0,0)
    loop 
      ige r1.w, r0.w, l(200)
      breakc_nz r1.w
      add r2.xyz, r0.xyzx, v1.xyzx
      add r0.xyz, r1.xyzx, r2.xyzx
      iadd r0.w, r0.w, l(1)
    endloop
    mov o1.xyz, r0.xyzx
    ret 
    // Approximately 19 instruction slots used

元のソースコードに loop 展開のアトリビュートを次のように追加すると

Out.Normal= 0;
[unroll] // attribute
for( int i= 0 ; i< 200 ; i++ ){
	Out.Normal+= In.Normal;
	Out.Normal+= wpos;
}

145 usec と実行時間が一気に 1/10 になりました。
出力コードを見てみると・・

   vs_4_0
    dcl_input v0.xyz
    dcl_input v1.xyz
    dcl_output_siv o0.xyzw , position
    dcl_output o1.xyz
    dcl_constantbuffer cb0[8], immediateIndexed
    dcl_constantbuffer cb1[3], immediateIndexed
    dcl_temps 3
    mov r0.xyz, v0.xyzx
    mov r0.w, l(1.000000)
    dp4 r1.y, cb1[1].xyzw, r0.xyzw
    mul r2.xyzw, r1.yyyy, cb0[5].xyzw
    dp4 r1.x, cb1[0].xyzw, r0.xyzw
    dp4 r1.z, cb1[2].xyzw, r0.xyzw
    mad r0.xyzw, cb0[4].xyzw, r1.xxxx, r2.xyzw
    mad r0.xyzw, cb0[6].xyzw, r1.zzzz, r0.xyzw
    add o0.xyzw, r0.xyzw, cb0[7].xyzw
    mul r0.xyz, v1.xyzx, l(200.000000, 200.000000, 200.000000, 0.000000)
    mad o1.xyz, r1.xyzx, l(200.000000, 200.000000, 200.000000, 0.000000), r0.xyzx
    ret 
    // Approximately 12 instruction slots used

当たり前です。これは元の例題が悪かったです。
ただの積和なので畳み込まれてしまいました。
ここまできちんとオプティマイズかかるんですね。
逆に attribute が [loop] (または無指定) だと意味のない演算でも
そのままループに展開されてしまうわけです。

ほんのわずかに複雑なコードにしてみます。

for( int i= 0 ; i< 200 ; i++ ){
	Out.Normal+= Temp[i&3];
	Out.Normal+= wpos;
}

これでもまだ法則性があるので最適化の余地があります。
[loop] で 20slot、[unroll] で 409slot の命令になります。
速度は2倍差ほど。

[loop]    20 slot  1652 usec
[unroll] 409 slot   806 usec

[unroll] だとこんな感じに展開されています。

    add r0.xyz, r0.xyzx, cb1[0].xyzx
    add r0.xyz, r1.xyzx, r0.xyzx
    add r0.xyz, r0.xyzx, cb1[1].xyzx
    add r0.xyz, r1.xyzx, r0.xyzx
    add r0.xyz, r0.xyzx, cb1[2].xyzx
    add r0.xyz, r1.xyzx, r0.xyzx
    add r0.xyz, r0.xyzx, cb1[3].xyzx
    :

さらに法則性を取り除きます。

for( int i= 0 ; i< 200 ; i++ ){
	Out.Normal+= Temp[(int)(Pos.x*i)&3];
	Out.Normal+= wpos;
}

これだとおそらく [unroll] でも極端な差が出ないと予想できます。
逆転しました。

[loop]     23 slot   3296 usec
[unroll]  564 slot  12786 usec

[unroll] 側の時間増加が極端なので何か他に原因がありそうです。
使用する命令スロット数の増加もペナルティがあるのかもしれません。
ループ回数を変えて計測してみました。

横loop回数/縦usec

 loop  u-slot [unroll]  l-slot  [loop]
   40    124      565      23     651 usec
   60    179      866      23     967
   80    234     1488      23    1284
  100    289     2653      23    1600
  120    344     3320      23    1915
  140    399     5810      23    2232
  160    454     8430      23    2558
  180    509    10730      23    2882
  200    564    12786      23    3296

80 と 120 前後で大きな変化があるようです。slot 数でいえば
234~399のあたりです。
この数値を目安にして別の演算でも同じ傾向が出るか確認してみます。
これも unroll でリニアなコードに展開されます。

Out.Normal= 0;
for( int i= 0 ; i< 100 ; i++ ){
	Out.Normal+= pow( i, In.Pos.x );
}

横loop回数/縦usec

 loop  u-slot  [unroll]  [loop]
  100    159      252      843 usec
  200    309      532     1656
  300    458      824     2482
  400    609     1130     3323
  500    759     1400     4263
  600    909     1686     5120
  700   1059     1988     5970
  800   1209     2290     6678

きれいなリニアです。命令は 1000slot 超えても問題ないし、
しかも unroll の方が速いし、命令スロット数はまったく影響を
与えていないように見えます。

[unroll] で遅くなる先ほどの shader が、unroll すると意外に
temporary register を消費していることがわかりました。

横loop回数/縦tempreg数

          loop  slot   time  temp
[unroll]   40   124     565     5
[unroll]   60   179     866     7
[unroll]   80   234    1488    12
[unroll]  100   289    2653    15
[unroll]  120   344    3320    17
[unroll]  140   399    5810    18
[unroll]  160   454    8430    18
[unroll]  180   509   10730    18
[unroll]  200   564   12786    18

80~120 前後での急激な負荷上昇と一致します。どうやら速度低下の
原因は、セオリー通り temporary register 数だったようです。
単なるループ展開だと思って見落としていました。

結論は、[unroll] の方が高速。だけど最適化によって temp register
が増えてしまうくらいなら素直に [loop] した方がまし。

unroll するけど積極的な最適化をしない attribute もあると
もう少し違ってくる可能性があります。
最適化レベルの /O0~/O3 はとくに変化が見られませんでした。
[unroll] かつ /Od が一番近い結果になりますが、実行すると差が
でません。内部で最適化かかってしまっていたのか、切り替えがうまく
機能していなかったのか、本当に同じ速度だったのか、
まだまだ調べる余地がありそうです。