For sse/neon it is faster to pad your matrices to a power of two, at least in my own game that was the case, also you can do a transpose and a dot product, obviously check on godbolt, but this is probably better.
godbolt
basically the padded version does this on release fast
pub const Mat3Padded = struct {
rows: [3]@Vector(4, f32),
};
export fn padded(
m1: *const Mat3Padded,
m2: *const Mat3Padded,
out: *Mat3Padded,
) void {
const Vec = @Vector(4, f32);
inline for (0..3) |i| {
const s0: Vec = @splat(m1.rows[i][0]);
const s1: Vec = @splat(m1.rows[i][1]);
const s2: Vec = @splat(m1.rows[i][2]);
out.rows[i] =
s0 * m2.rows[0] +
s1 * m2.rows[1] +
s2 * m2.rows[2];
}
}
compiles down to this
example.padded:
push rbp
mov rbp, rsp
vbroadcastss xmm0, dword ptr [rdi]
vbroadcastss xmm1, dword ptr [rdi + 4]
vbroadcastss xmm2, dword ptr [rdi + 8]
vmulps xmm0, xmm0, xmmword ptr [rsi]
vmulps xmm1, xmm1, xmmword ptr [rsi + 16]
vaddps xmm0, xmm0, xmm1
vmulps xmm1, xmm2, xmmword ptr [rsi + 32]
vaddps xmm0, xmm0, xmm1
vmovaps xmmword ptr [rdx], xmm0
vbroadcastss xmm0, dword ptr [rdi + 16]
vbroadcastss xmm1, dword ptr [rdi + 20]
vmulps xmm0, xmm0, xmmword ptr [rsi]
vmulps xmm1, xmm1, xmmword ptr [rsi + 16]
vbroadcastss xmm2, dword ptr [rdi + 24]
vaddps xmm0, xmm0, xmm1
vmulps xmm1, xmm2, xmmword ptr [rsi + 32]
vaddps xmm0, xmm0, xmm1
vmovaps xmmword ptr [rdx + 16], xmm0
vbroadcastss xmm0, dword ptr [rdi + 32]
vbroadcastss xmm1, dword ptr [rdi + 36]
vbroadcastss xmm2, dword ptr [rdi + 40]
vmulps xmm0, xmm0, xmmword ptr [rsi]
vmulps xmm1, xmm1, xmmword ptr [rsi + 16]
vmulps xmm2, xmm2, xmmword ptr [rsi + 32]
vaddps xmm0, xmm0, xmm1
vaddps xmm0, xmm0, xmm2
vmovaps xmmword ptr [rdx + 32], xmm0
pop rbp
ret
And your version unpadded does
pub const Mat3x3 = [3][3]f32;
export fn baseline(m1: *const Mat3x3, m2: *const Mat3x3, out: *Mat3x3) void {
const Vec = @Vector(3, f32);
const m2v: [3]Vec = .{
@as(Vec, m2[0]),
@as(Vec, m2[1]),
@as(Vec, m2[2]),
};
inline for (0..3) |i| {
const s0: Vec = @splat(m1[i][0]);
const s1: Vec = @splat(m1[i][1]);
const s2: Vec = @splat(m1[i][2]);
out[i] =
s0 * m2v[0] +
s1 * m2v[1] +
s2 * m2v[2];
}
}
which compiles down to :
example.baseline:
push rbp
mov rbp, rsp
vmovsd xmm0, qword ptr [rsi + 4]
vmovss xmm1, dword ptr [rsi]
vmovlhps xmm1, xmm1, xmm0
vshufps xmm0, xmm1, xmm0, 216
vmovsd xmm1, qword ptr [rsi + 16]
vmovss xmm2, dword ptr [rsi + 12]
vmovlhps xmm2, xmm2, xmm1
vshufps xmm1, xmm2, xmm1, 216
vmovsd xmm2, qword ptr [rsi + 28]
vmovss xmm3, dword ptr [rsi + 24]
vmovlhps xmm3, xmm3, xmm2
vshufps xmm2, xmm3, xmm2, 216
vbroadcastss xmm3, dword ptr [rdi]
vbroadcastss xmm4, dword ptr [rdi + 4]
vbroadcastss xmm5, dword ptr [rdi + 8]
vmulps xmm3, xmm0, xmm3
vmulps xmm4, xmm1, xmm4
vaddps xmm3, xmm3, xmm4
vmulps xmm4, xmm2, xmm5
vaddps xmm3, xmm3, xmm4
vmovlps qword ptr [rdx], xmm3
vextractps dword ptr [rdx + 8], xmm3, 2
vbroadcastss xmm3, dword ptr [rdi + 12]
vbroadcastss xmm4, dword ptr [rdi + 16]
vbroadcastss xmm5, dword ptr [rdi + 20]
vmulps xmm3, xmm0, xmm3
vmulps xmm4, xmm1, xmm4
vaddps xmm3, xmm3, xmm4
vmulps xmm4, xmm2, xmm5
vaddps xmm3, xmm3, xmm4
vmovlps qword ptr [rdx + 12], xmm3
vextractps dword ptr [rdx + 20], xmm3, 2
vbroadcastss xmm3, dword ptr [rdi + 24]
vbroadcastss xmm4, dword ptr [rdi + 28]
vbroadcastss xmm5, dword ptr [rdi + 32]
vmulps xmm0, xmm0, xmm3
vmulps xmm1, xmm1, xmm4
vaddps xmm0, xmm0, xmm1
vmulps xmm1, xmm2, xmm5
vaddps xmm0, xmm0, xmm1
vmovlps qword ptr [rdx + 24], xmm0
vextractps dword ptr [rdx + 32], xmm0, 2
pop rbp
ret
So basically the padded version avoids all the unpacking whereas the normal version has to build the xmm registers three time
In fact you can even go further with FMA
pub const Vec4 = @Vector(4, f32);
export fn paddedFma(
a: *const Mat3Padded,
b: *const Mat3Padded,
out: *Mat3Padded,
) void {
const b0 = b.rows[0];
const b1 = b.rows[1];
const b2 = b.rows[2];
inline for (0..3) |i| {
const a0: Vec4 = @splat(a.rows[i][0]);
const a1: Vec4 = @splat(a.rows[i][1]);
const a2: Vec4 = @splat(a.rows[i][2]);
var r = a0 * b0;
r = @mulAdd(Vec4, a1, b1, r);
r = @mulAdd(Vec4, a2, b2, r);
out.rows[i] = r;
}
}
example.paddedFma:
push rbp
mov rbp, rsp
vmovaps xmm0, xmmword ptr [rsi]
vmovaps xmm1, xmmword ptr [rsi + 16]
vmovaps xmm2, xmmword ptr [rsi + 32]
vmulps xmm3, xmm0, dword ptr [rdi]{1to4}
vfmadd231ps xmm3, xmm1, dword ptr [rdi + 4]{1to4}
vfmadd231ps xmm3, xmm2, dword ptr [rdi + 8]{1to4}
vmovaps xmmword ptr [rdx], xmm3
vmulps xmm3, xmm0, dword ptr [rdi + 16]{1to4}
vfmadd231ps xmm3, xmm1, dword ptr [rdi + 20]{1to4}
vfmadd231ps xmm3, xmm2, dword ptr [rdi + 24]{1to4}
vmovaps xmmword ptr [rdx + 16], xmm3
vmulps xmm0, xmm0, dword ptr [rdi + 32]{1to4}
vfmadd231ps xmm0, xmm1, dword ptr [rdi + 36]{1to4}
vfmadd231ps xmm0, xmm2, dword ptr [rdi + 40]{1to4}
vmovaps xmmword ptr [rdx + 32], xmm0
pop rbp
ret
Hope i didn’t make mistakes but this is what I use found it in the valve’s Source code and absolutely stole it 