I wrote a function to batch-transform 3D vectors by a single 3x4 matrix using SSE2:
struct alignas(16) Matrix3x4f
{
union
{
Vector4 r[3]; // (r[0].w, r[1].w, r[2].w) contains the translation
float m[3][4];
};
// (0, 0, 0, 1) is the forth row of the full 4x4 matrix
};
struct Positions_SoA
{
Vector4 * xs;
Vector4 * ys;
Vector4 * zs;
};
void transform_points_SSE2(
const Matrix3x4f& transform_,
const Positions_SoA& inputs_,
const int number_of_packets_,
Positions_SoA &outputs_
)
{
//FIXME: woudn't this cause too much register pressure and memory loads?
// For AVX/AVX2 we'll have to splat 8/16 values.
const Vector4 m00 = SPLAT_X( transform_.r[0] );
const Vector4 m01 = SPLAT_Y( transform_.r[0] );
const Vector4 m02 = SPLAT_Z( transform_.r[0] );
const Vector4 m03 = SPLAT_W( transform_.r[0] ); // translation X
const Vector4 m10 = SPLAT_X( transform_.r[1] );
const Vector4 m11 = SPLAT_Y( transform_.r[1] );
const Vector4 m12 = SPLAT_Z( transform_.r[1] );
const Vector4 m13 = SPLAT_W( transform_.r[1] ); // translation Y
const Vector4 m20 = SPLAT_X( transform_.r[2] );
const Vector4 m21 = SPLAT_Y( transform_.r[2] );
const Vector4 m22 = SPLAT_Z( transform_.r[2] );
const Vector4 m23 = SPLAT_W( transform_.r[2] ); // translation Z
for( int i = 0; i < number_of_packets_; i++ )
{
outputs_.xs[i] = V4_ADD(
V4_ADD(
V4_MUL( m00, inputs_.xs[i] ),
V4_MUL( m01, inputs_.ys[i] )
),
V4_ADD(
V4_MUL( m02, inputs_.zs[i] ),
m03
)
);
outputs_.ys[i] = V4_ADD(
V4_ADD(
V4_MUL( m10, inputs_.xs[i] ),
V4_MUL( m11, inputs_.ys[i] )
),
V4_ADD(
V4_MUL( m12, inputs_.zs[i] ),
m13
)
);
outputs_.zs[i] = V4_ADD(
V4_ADD(
V4_MUL( m20, inputs_.xs[i] ),
V4_MUL( m21, inputs_.ys[i] )
),
V4_ADD(
V4_MUL( m22, inputs_.zs[i] ),
m23
)
);
}
}
The complete, runnable code is on rextester.
When I run the tests and compare the numbers with the reference (scalar) version, the results of the SSE version appear slightly wrong in half of all the cases, although the printed numbers look the same. Is that due to rounding errors? (FPU internally uses 80-bit precision, and SSE units compute with 32-bit precision.) Should I ignore those small discrepancies? Is my 'optimized' function even correct?
Why is the speedup due to SSE so small? I expected 3-3.5 speed increase, but got less than 2. Should I use AVX?
I plan to transform up to 64 points, is it still worth to use SSE/AVX? Will performance be dominated by the cost of loading into registers? I'm worried about the 12 shuffle/broadcast/replicate instructions.
Finally, I'll appreciate comments on my programming style, my mistakes in using SSE, a note on best practices and performance pitfalls.