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

Microsoft Gamefest Japan 2007

Shader.jp さんからの情報ですが
ゲーム開発者のための技術説明会 「Gamefest Japan 2007」 開催

CEDEC から Meltdown が消えたなと思っていたら、独自に
Gamefest Japan を行うみたいですね。
二日間にわたって開催されて結構内容も幅広いようです。
9月は忙しくなりそうです。

ゲーム開発のための技術説明会 Gamefest Japan 2007 開催 ゲーム制作技術情報を日本で初めて広く一般へ公開
こちらにも書いてあるように Gamefest はもともと開発者限定の
クローズドなイベントだったので、それが広く一般に公開された形です。
ターゲットはやはり開発者で、マルチプラットフォーム化等の推進が
狙いとのことです。

そういえばゲームは、Wii/DS 等ユーザー層の拡大がここ最近のテーマでした。
もしかしたら MS は XNA 関連を中心にして、ユーザーだけでなく
開発の一般への浸透化や、開発者の層の拡大もある程度視野に入れて
いるのかもしれません。

Direct3D 10 GeForce8800GTX は GTS の 1.5倍速い

前々回
Direct3D 10 Shader4.0 ループと最適化
で行ったテストを、GeForce8800GTX でも試してみました。

テストしたのは、逆転して [loop] の方が高速になる 3番目の
shader です。GeForce8800GTX の傾向は GTS とまったく同じで、
loop 120 回以上で速度低下が顕著になります。

Unrollと速度

・縦軸は実行にかかった時間(usec)。位置が低い方が高速。
・横軸はループ回数

GeForce8800GTX
loop回数  time    GTS比
    40     371     1.52
    60     570     1.52
    80     962     1.55
   100    1752     1.51
   120    2120     1.57
   140    3870     1.50
   160    5620     1.50
   180    7200     1.49
   200    8780     1.45

このデータを見る限り、temporary register の割り当てに関しては
特に GTS と GTX で差がないように見えます。つまりシェーダー
ユニットに対する、割り当て可能な register pool の割合はおそらく
同率になっているのでしょう。
GTS はシェーダーユニット数が少ないけれど、その分潤沢にレジスタを
使えるわけではありませんでした。

数値を取っていて気になったのは、思った以上に GTX が速いという
ことです。上のグラフには前々回の GTS の結果も重ねており、また
表には GTS 比でどれくらい速いのかも書き込んでみました。

だいたい 1.5倍程度 GTX の方が高速に動作している計算です。
こんなに差があったかな・・と思ってスペックを確認してみました。

DirectX10 GPU メモ

              stream processor  shader clock  memory clock  mem-bus
GeForce8800Ultra    128sp         1500MHz       1080MHz      384bit
GeForce8800GTX      128sp         1350MHz        900MHz      384bit
GeForce8800GTS       96sp         1200MHz        800MHz      320bit

Stream Processor の数で 約1.33 倍
Shader Clock の差で 1.125 倍

1.33 × 1.125 ≒ 1.50

ぴったり計算どおりでした。

シェーダーの実行速度に対して、メモリ速度は clock で 1.125倍、
bus 幅で 1.2倍、あわせて 1.35 倍です。

シェーダーの演算ではなくメモリが足を引っ張る状況では、GTS と
GTX の速度差はもっと小さくなります。
普段体感している速度差は、おそらくこちらの方が近いのではない
でしょうか。

例えば前回(昨日)
Direct3D 10 Shader4.0 補間レジスタ数と速度の関係
のグラフをもう一度じっくり見てみます。

出力レジスタの個数と速度

出力数が 11以上の右側では、GTS と GTX の差を計算してみると
ちょうど 1.5倍前後になっていることがわかります。
ここは上と同じで計算どおりなので、純粋にシェーダーの演算能力
で頭打ちになっているといえるでしょう。

それに比べて左側、11未満の結果では、GTS と GTX の差がほとんど
ありません。GTX の速度は GTS 比でわずか 1.06~1.1 倍程度です。

ほぼこれは Core や Shader、Memory 等の Clock 比 1.125倍に相当
すると考えられます。

つまりここでのボトルネックは、core か Shader Unit 内部に存在
する固定ユニットの実行速度なのでしょう。GTS でも GTX でも GPU
内にたぶん同一個数実装されていると推測できます。

もし仮にこれが 1.35倍に開いていたら、それはおそらくメモリが
足を引っ張っている部分です。

これらの結果から GTX は GTS と比較して、状況によって
1.125~1.5 倍高速に実行できます。メモリ速度を考えると、本当に
1.5倍の速度差がでるようなケースはそれほど多くないと思います。

また同じように GeForce8800Ultra で計算すると、GTS 比で次のよう
になります。

Shader 1.66倍
Memory 1.62倍
Clock 1.224~1.35

これは速いですね。メモリ速度も Shader 並みの比率を保っているので、
比較的1.6倍に近いスループットが期待できそうです。

Direct3D 10 Shader4.0 補間レジスタ数と速度の関係

コードを書いていて気になったので少々実験してみました。
VertexShader または GeometryShader が出力する値はラスタライザ
(RasterizerStage) に渡ります。
PixelShader は各ピクセルごとに補間された値を受け取ります。

この VertexShader の出力数値の数と描画速度の関係を調べてみました。
ここでは GeometryShader は NULL にしています。

横軸がシェーダーで使用した出力レジスタの総数、
縦がかかった時間(usec)です。

出力レジスタ数と速度の関係

出力数 命令数  88GTS   88GTX
   2     12     145     136 usec
   3     13     150     130
   4     15     147     138
   5     16     148     140
   6     17     148     134
   7     18     148     140
   8     19     157     135
   9     20     164     135
   10    21     173     135
   11    22     335     221
   12    23     347     229
   13    24     360     237
   14    25     376     248
   15    26     398     263
   16    27     410     271

・88GTS = GeForce8800GTS 640 の結果
・88GTX = GeForce8800GTX の結果
・GTS固定 = GeForce8800GTS で、出力レジスタ数を2個固定にし、
 命令数だけ増やしていったもの。

88GTS で出力レジスタ数が2~7個の間特に変化がないのは、動作時間へ
与える影響が他の実行ボトルネックに隠れてしまったためと考えられます。
7~10 までの緩やかな上昇がそれ以前も継続していた可能性が高いです。

その緩やかな上昇の要因として、出力レジスタ数の増加だけではなく
実行する命令数そのものが増えたことも考慮しなければなりません。

そのため出力レジスタ数を2個固定にして、プログラムの長さ(slot数)
を 12~27 まで変化させて同じように速度を量ってみました。
それがグラフ中の「GTS固定」です。
これを見るとほぼ一定なので、やはり10個以前でも出力レジスタ数が
動作に何らかの影響を与えている可能性があります。

注目すべきところはやっぱり出力数 10を超えたところで生じる急激な
変化です。ハード的なバッファの限界、同時に実行可能な補間ユニット
の数、内部バス転送能力の限界、等が考えられます。

VertexShader そのものの命令数が増加して実行時間が長くなれば、
それだけ Rasterizer にも余裕ができます。当初は、その拮抗点が
ちょうど10個目の位置にあったのではないかと仮定しました。
だけど「GTS固定」の結果を見ると、この程度の命令数ではほとんど
実行に影響を与えていないことがわかります。
よって 10 はハード的なマジックナンバーである可能性が高いです。

なお、あとから GeForce8800GTX でも同等のテストを行ってみましたが、
10個を越えた時点で発生する大きな変化は一緒でした。
(グラフに加えました)

ShaderModel 毎に対応している補間レジスタ数は次のとおりです。

ShaderModel2.0
・clmap された COLOR0~1 (2個)
・TEXCOORD0~7 (8個)
・POSITION、FOG

ShaderModel3.0
・自由に割り振れる 12個 (o0~11)

ShaderModel4.0
・自由に割り振れる 16個 (o0~o15)

ShaderModel 2.0~3.0 では、たぶんほとんどのケースで出力レジスタ
数は 10個以内に収まってしまうでしょう。
GeForce68/78 など 3.0 当時の設計でも、仕様上の上限よりは若干低い
位置にハードウエア的な制限があったと考えられます。
GeForce8800 で従来のシェーダーも非常に高速なのは、もしかしたら
こんなところにも原因があるのかもしれません。

今回のテストの結論は、もし速度を追求するなら出力レジスタ数は
10個以内に収めましょう、ということです。

ただ、これはあくまでボトルネックになる得る条件の1つに過ぎないので、
通常はピクセル面積ももっと大きいし、VertexShader も長くなるしと
他の実行サイクルの影に隠れてあまり表に出てこない可能性があります。

相殺されている分には実質的な負荷ではないので、高速化を考える場合
はこのあたりのさじ加減が難しいですね。

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 が一番近い結果になりますが、実行すると差が
でません。内部で最適化かかってしまっていたのか、切り替えがうまく
機能していなかったのか、本当に同じ速度だったのか、
まだまだ調べる余地がありそうです。