日別アーカイブ: 2009年8月7日

Direct3D Matrix の並びと SSE 命令

Matrix の並びには row major と column major の2種類あります。
たまにどっちがどっちかわからなくなりますが、row major が「三」で
column major が「川」。
演算上はどちらも違いは無くデータ配列が異なっているだけです。
特にシェーダーのコード上は大差ないです。演算もデータ構造も使う側に委ねられて
いてどちらでも使えます。

シェーダーコンパイラ fxc は /Zpr, /Zpc のオプションで並びの方向を指定する
ことが出来ます。
このオプションによって、たとえば vector * matrix の乗算が dp (dot product)
に展開されるか mad に展開されるか切り替わります。
どっちの設定だろうが乗算の前後を入れ替えれば良いだけなので、正直自分の記述と
デフォルトが異なっている場合にオプションで入れ替えればよい、とそのくらいの
認識でした。

fxc のデフォルトは /Zpc で column-major になっています。
上の演算であれば mad に展開されます。

シェーダーの場合 dp, mad どちらでも基本的に演算命令数は変わらず、効率の差は
生じません。ただ mad の方が vector の w=1 の代入が簡略化されるケースがある
ため小さくなることがあります。
これがそのまま GPU ネイティブコード上でも影響があるかどうかはまた別です。
AMD stream のように swizzle 時に直接 w に 1 を書き込めるなら差は無くなると
思います。

; opos= mul( mat, float4(v.xyz,1) );

; /Zpr
mov r0.xyz, cb0[4].xyzx
mov r0.w, l(1.000000)
dp4 o0.x, cb0[0].xyzw, r0.xyzw
dp4 o0.y, cb0[1].xyzw, r0.xyzw
dp4 o0.z, cb0[2].xyzw, r0.xyzw
dp4 o0.w, cb0[3].xyzw, r0.xyzw

; /Zpc
mul r0.xyzw, cb0[1].xyzw, cb0[4].yyyy
mad r0.xyzw, cb0[0].xyzw, cb0[4].xxxx, r0.xyzw
mad r0.xyzw, cb0[2].xyzw, cb0[4].zzzz, r0.xyzw
add o0.xyzw, r0.xyzw, cb0[3].xyzw

Direct3D ではもともと column major の形式で D3DMATIRX が定義されていました。
D3D9 以前、固定機能パイプラインでは API に渡すデータ並びを共通化する必要が
あったためと思われます。
そのため自前のライブラリでもほぼ D3DMATRIX 互換で使っていました。

typedef struct _D3DMATRIX {
    union {
        struct {
            float   _11, _12, _13, _14;
            float   _21, _22, _23, _24;
            float   _31, _32, _33, _34;
            float   _41, _42, _43, _44;

        };
        float m[4][4];
    };
} D3DMATRIX;

ただし実際はターゲットハードウエアやライブラリの仕様に合わせたり、最適化の
ために row major 形式に変換することが多かった思います。
シェーダーの演算上は大差なくても contant buffer の利用効率に影響があるからです。
昔は Constant Buffer が非常に小さかったため、index で参照する matrix は
4×3 でないとほとんど入りませんでした。
そのため Bone として使う geometry 用の matrix 乗算ルーチンをライブラリに
加えています。これは乗算結果を転置の 4×3 で返す専用命令です。

そろそろ D3DMATRIX 互換性もいらないかなと思い、最初から全部 row major で
持つようにライブラリを書き換えてみました。

基本的には自前の Matrix を下記のように定義し直すだけで、メンバにアクセスして
いるコードはほぼそのまま動きます。

struct {
    float  _11, _21, _31, _41;
    float  _12, _22, _32, _42;
    float  _13, _23, _33, _43;
    float  _14, _24, _34, _44;
};

修正が必要なのは、部分的に vector みなしてアクセスしていた処理と、SSE 命令で
記述していた各種関数です。
4×4 の matrix 同士の乗算では修正は不要ですが、それ以外はいろいろ見直す必要が
ありそうです。
たとえば vector との乗算だと、mad ではなく dp 相当の水平演算になります。

SSE4.1 の dpps を使ってみました。SSE4 のヘッダは smmintrin.h。
実命令数がわかるように dest 側のレジスタを共通にしています。

// SSE4.1
__m128 xmm0;
xmm0= _mm_load_ps( &v.x );

xmm1= _mm_load_ps( &_11 );
xmm1= _mm_dp_ps( xmm1, xmm0, 0xf1 );

xmm2= _mm_load_ps( &_12 );
xmm2= _mm_dp_ps( xmm2, xmm0, 0xf2 );
xmm1= _mm_add_ps( xmm1, xmm2 );

xmm2= _mm_load_ps( &_13 );
xmm2= _mm_dp_ps( xmm2, xmm0, 0xf4 );
xmm1= _mm_add_ps( xmm1, xmm2 );

xmm2= _mm_load_ps( &_14 );
xmm2= _mm_dp_ps( xmm2, xmm0, 0xf8 );
xmm1= _mm_add_ps( xmm1, xmm2 );

Shader のように簡単に記述できますが SSE4.1 対応 CPU (Penryn 以降) でないと
動きません。
試しに SSE3 の haddps を使ってみました。

// SSE3
xmm0= _mm_load_ps( &v.x );

xmm1= _mm_load_ps( &_11 );
xmm1= _mm_mul_ps( xmm1, xmm0 );

xmm2= _mm_load_ps( &_12 );
xmm2= _mm_mul_ps( xmm2, xmm0 );

xmm1= _mm_hadd_ps( xmm1, xmm2 );

xmm3= _mm_load_ps( &_13 );
xmm3= _mm_mul_ps( xmm3, xmm0 );

xmm2= _mm_load_ps( &_14 );
xmm2= _mm_mul_ps( xmm2, xmm0 );

xmm3= _mm_hadd_ps( xmm3, xmm2 );
xmm1= _mm_hadd_ps( xmm1, xmm3 );

全く同じ命令数でした。
SSE3 対応 PC の方が多いしこっちの方が良さそうです。
SSE4 命令は命令長が長いので生成バイナリも小さくなります。
ただもっと複雑なケースだと、入力を自由に選択できたり出力マスク可能な dpps
の方が融通が利いて少ない命令で書けるようです。
以前はこんな感じ。

// SSE1 (column major)
xmm0= _mm_load_ps( &v.x );

xmm1= xmm0;
xmm1= _mm_shuffle_ps( xmm1, xmm1, _MM_SHUFFLE(0,0,0,0) );
xmm2= _mm_load_ps( &_11 );
xmm1= _mm_mul_ps( xmm1, xmm2 );

xmm3= xmm0;
xmm3= _mm_shuffle_ps( xmm3, xmm3, _MM_SHUFFLE(1,1,1,1) );
xmm2= _mm_load_ps( &_21 );
xmm3= _mm_mul_ps( xmm3, xmm2 );
xmm1= _mm_add_ps( xmm1, xmm3 );

xmm3= xmm0;
xmm3= _mm_shuffle_ps( xmm3, xmm3, _MM_SHUFFLE(2,2,2,2) );
xmm2= _mm_load_ps( &_31 );
xmm3= _mm_mul_ps( xmm3, xmm2 );
xmm1= _mm_add_ps( xmm1, xmm3 );

xmm0= _mm_shuffle_ps( xmm0, xmm0, _MM_SHUFFLE(3,3,3,3) );
xmm2= _mm_load_ps( &_41 );
xmm0= _mm_mul_ps( xmm0, xmm2 );
xmm1= _mm_add_ps( xmm1, xmm0 );

matrix 同士の乗算も水平演算命令で書けるかと思い少々考えてみましたがレジスタ
が足りなくてうまくいっていません。
x64 だとレジスタ数が倍あるため intrinsic で書いたとおりのコードに展開されます。
x86 では待避のためのスタックが使われていました。

関連エントリ
Intel AVX その3 命令
D3D10 row_major column_major
SSE についてのメモ(2) SSE4など