I recently started toying with SIMD and came up with the following code for matrix multiplication.
First I attempted to implement it using SIMD the same way I did in SISD, just using SIMD for things like the dot product for each particular entry, which was actually slower (still trying to figure this one out).
After giving it some thought I realized I could do the calculation for the resulting matrix row-by-row instead, by lining up the registers something like this (each row is one SIMD register, and each column is the x, y, z, w parts):
With matrices \$A\$, \$B\$ and computing \$C = A * B\$:
A_00 * B_00 A_00 * B_01 + + A_01 * B_10 A_01 * B_11 + + A_02 * B_20 A_02 * B_21 + + A_03 * B_30 A_03 * B_31 = = C_00 = Dot(A_Row0, B_Col0), C_01 = Dot(A_Row0, B_Col1), ... A_10 * B_00 A_10 * B_01 + + A_11 * B_10 A_11 * B_11 + + ... ... C_10 = Dot(A_Row1, B_Col0), C_11 = Dot(A_Row1, B_Col1), ...
I would greatly appreciate if someone with more experience with these things could tell me how far off I am from a good solution.
__m128 BCx = _mm_load_ps((float*)&B.Row0);
__m128 BCy = _mm_load_ps((float*)&B.Row1);
__m128 BCz = _mm_load_ps((float*)&B.Row2);
__m128 BCw = _mm_load_ps((float*)&B.Row3);
// Calculate Row0 in resulting matrix
__m128 ARx = _mm_set1_ps(A.Row0.X);
__m128 ARy = _mm_set1_ps(A.Row0.Y);
__m128 ARz = _mm_set1_ps(A.Row0.Z);
__m128 ARw = _mm_set1_ps(A.Row0.W);
__m128 X = _mm_mul_ps(ARx, BCx);
__m128 Y = _mm_mul_ps(ARy, BCy);
__m128 Z = _mm_mul_ps(ARz, BCz);
__m128 W = _mm_mul_ps(ARw, BCw);
__m128 R = _mm_add_ps(X, _mm_add_ps(Y, _mm_add_ps(Z, W)));
_mm_storeu_ps((float*)&Result.Row0, R);
// Calculate Row1 in resulting matrix
ARx = _mm_set1_ps(A.Row1.X);
ARy = _mm_set1_ps(A.Row1.Y);
ARz = _mm_set1_ps(A.Row1.Z);
ARw = _mm_set1_ps(A.Row1.W);
X = _mm_mul_ps(ARx, BCx);
Y = _mm_mul_ps(ARy, BCy);
Z = _mm_mul_ps(ARz, BCz);
W = _mm_mul_ps(ARw, BCw);
R = _mm_add_ps(X, _mm_add_ps(Y, _mm_add_ps(Z, W)));
_mm_storeu_ps((float*)&Result.Row1, R);
// Calculate Row2 in resulting matrix
ARx = _mm_set1_ps(A.Row2.X);
ARy = _mm_set1_ps(A.Row2.Y);
ARz = _mm_set1_ps(A.Row2.Z);
ARw = _mm_set1_ps(A.Row2.W);
X = _mm_mul_ps(ARx, BCx);
Y = _mm_mul_ps(ARy, BCy);
Z = _mm_mul_ps(ARz, BCz);
W = _mm_mul_ps(ARw, BCw);
R = _mm_add_ps(X, _mm_add_ps(Y, _mm_add_ps(Z, W)));
_mm_storeu_ps((float*)&Result.Row2, R);
// Calculate Row3 in resulting matrix
ARx = _mm_set1_ps(A.Row3.X);
ARy = _mm_set1_ps(A.Row3.Y);
ARz = _mm_set1_ps(A.Row3.Z);
ARw = _mm_set1_ps(A.Row3.W);
X = _mm_mul_ps(ARx, BCx);
Y = _mm_mul_ps(ARy, BCy);
Z = _mm_mul_ps(ARz, BCz);
W = _mm_mul_ps(ARw, BCw);
R = _mm_add_ps(X, _mm_add_ps(Y, _mm_add_ps(Z, W)));
_mm_storeu_ps((float*)&Result.Row3, R);
Using full optimization (/Ox
) on the Visual Studio 2013 compiler, this is roughly twice as fast as my typical SISD version (not really sure how much to expect?).
Here is my SISD version:
inline Mat4*
Mat4Mul(const Mat4 *M0, const Mat4 *M1, Mat4 *Out)
{
Vec4 Col0 = {M1->M00, M1->M10, M1->M20, M1->M30};
Vec4 Col1 = {M1->M01, M1->M11, M1->M21, M1->M31};
Vec4 Col2 = {M1->M02, M1->M12, M1->M22, M1->M32};
Vec4 Col3 = {M1->M03, M1->M13, M1->M23, M1->M33};
Out->M00 = Vec4Dot(&M0->Row0, &Col0);
Out->M01 = Vec4Dot(&M0->Row0, &Col1);
Out->M02 = Vec4Dot(&M0->Row0, &Col2);
Out->M03 = Vec4Dot(&M0->Row0, &Col3);
Out->M10 = Vec4Dot(&M0->Row1, &Col0);
Out->M11 = Vec4Dot(&M0->Row1, &Col1);
Out->M12 = Vec4Dot(&M0->Row1, &Col2);
Out->M13 = Vec4Dot(&M0->Row1, &Col3);
Out->M20 = Vec4Dot(&M0->Row2, &Col0);
Out->M21 = Vec4Dot(&M0->Row2, &Col1);
Out->M22 = Vec4Dot(&M0->Row2, &Col2);
Out->M23 = Vec4Dot(&M0->Row2, &Col3);
Out->M30 = Vec4Dot(&M0->Row3, &Col0);
Out->M31 = Vec4Dot(&M0->Row3, &Col1);
Out->M32 = Vec4Dot(&M0->Row3, &Col2);
Out->M33 = Vec4Dot(&M0->Row3, &Col3);
return Out;
}
DPPS
isn't efficient, though. On Intel Haswell, it has 14 cycle latency, and 2 cycles per insn throughput.MULPS
has 5 cycle latency and 0.5 cycles per insn throughput. (1/3 latency, 4x better throughput). See agner.org/optimize. If you have a lot of operations to do, do them vertically (fma, addps, mulps) and only operate horizontally (haddps / dpps) at the end. \$\endgroup\$