HLSL では matrix のフォーマットとして、宣言時に row_major と column_major
を選ぶことができます。例えば float4x4 (matrix) と float4 (vector) の乗算
ではどちらで宣言しても
・水平演算 dp4 命令を使うか
・並列に積和展開するか
の違いになります。命令ステップ数どちらもは 4 で動作上は影響が出ません。
以前こちらのメモに書いた 2種類のコード展開がちょうどこの
row_major と columns_major の違いに相当します。
・並列積和か水平か
(row_major, columns_major の違いだけでなく乗算順の設計にも依存する)
なので、エンジン側の設計に合わせて row_major にするか columns_major に
するか選ぶことができます。混在も可能でその辺は HLSL コンパイラがうまい
具合に合わせてくれます。
ただし入力が float3 になると少々話が違ってきます。
例えば VertexShader の入力頂点 float3 を、LocalToWorld float4x4 に
乗算する場合を考えます。
float4 wpos= mul( LocalToWorld, float4( Input.Position.xyz, 1 ) );
Output.Pos= mul( WorldToProjection, wpos );
このとき入力頂点の w に 1を代入し、matrix の translation 成分が
そのまま加算されるようにします。
このコードは、積和に水平展開されるとちょうど最後の積和が
単純な加算に置き換わるだけなのでコードのステップ数に影響が出ません。
vs_4_0
dcl_input v0.xyz
dcl_input v1.xyz
dcl_input v2.xy
dcl_output_siv o0.xyzw , position
dcl_output o1.xyz
dcl_output o2.xy
dcl_constantbuffer cb0[21], immediateIndexed
dcl_constantbuffer cb1[4], immediateIndexed
dcl_temps 2
mul r0.xyzw, v0.yyyy, cb1[1].xyzw
mad r0.xyzw, cb1[0].xyzw, v0.xxxx, r0.xyzw
mad r0.xyzw, cb1[2].xyzw, v0.zzzz, r0.xyzw
add r0.xyzw, r0.xyzw, cb1[3].xyzw // w=1 のときの加算
mul r1.xyzw, r0.yyyy, cb0[5].xyzw
mad r1.xyzw, cb0[4].xyzw, r0.xxxx, r1.xyzw
mad r1.xyzw, cb0[6].xyzw, r0.zzzz, r1.xyzw
mad o0.xyzw, cb0[7].xyzw, r0.wwww, r1.xyzw // ここは積和のまま
mov o1.xyz, v1.xyzx
mov o2.xy, v2.xyxx
ret
// Approximately 11 instruction slots used
ところが水平加算の dp4 に展開されると
vs_4_0
dcl_input v0.xyz
dcl_input v1.xyz
dcl_input v2.xy
dcl_output_siv o0.xyzw , position
dcl_output o1.xyz
dcl_output o2.xy
dcl_constantbuffer cb0[21], immediateIndexed
dcl_constantbuffer cb1[4], immediateIndexed
dcl_temps 2
mov r0.xyz, v0.xyzx // w を書き換えるためにテンポラリにコピー
mov r0.w, l(1.000000) // w=1 に代入
dp4 r1.x, cb1[0].xyzw, r0.xyzw
dp4 r1.y, cb1[1].xyzw, r0.xyzw
dp4 r1.z, cb1[2].xyzw, r0.xyzw
dp4 r1.w, cb1[3].xyzw, r0.xyzw
dp4 o0.x, cb0[4].xyzw, r1.xyzw
dp4 o0.y, cb0[5].xyzw, r1.xyzw
dp4 o0.z, cb0[6].xyzw, r1.xyzw
dp4 o0.w, cb0[7].xyzw, r1.xyzw
mov o1.xyz, v1.xyzx
mov o2.xy, v2.xyxx
ret
// Approximately 13 instruction slots used
w=1 の代入のために 2命令増えてしまいます。
消費するテンポラリ変数の個数は変わらないので、スレッド数の差は出ていない
と思われます。
ShaderModel1 の nVIDIA 拡張では、dp4 の時に vector 側の最後の成分を
強制的に 1 とみなす dph 命令がありました。もし dph 命令があったら
w=1 代入は不要なので、命令数に差は出ません。
命令の依存関係で見た場合は、mad よりも dp4 の方が直前の命令との相関性が
薄いので並列化では有利に見えます。もし依存関係によるパイプラインストール
が存在しているのなら dp4 の方が良いのかもしれません。
(とはいってもこの例ではバイパスが利くケースなのでストールは限られると
思います)
スレッドによる並列化で、そもそもパイプラインストールが存在しないなら
純粋に命令数が少ない mad の方がきっと速いのでしょう。
また実際はドライバによってさらに GPU 毎にネイティブな命令にコードに
置き換わっているので、このような単純な比較はできないのかもしれません。