Используя System.Runtime.Intrinsics.X86
, Sse2.MultiplyAddAdjacent
можно использовать для выполнения тяжелой работы, с некоторой перетасовкой et c для выравнивания данных. Например:
struct Vec2
{
public short X, Y;
}
struct Mat2x2
{
public short A, B, C, D;
}
static unsafe Vec2 Mul(Mat2x2 m, Vec2 v)
{
// movd: 0 0 0 0 0 0 Y X
var rawvec = Sse2.LoadScalarVector128((int*)&v);
// pshufd: Y X Y X Y X Y X
var vec = Sse2.Shuffle(rawvec, 0).AsInt16();
// movq: 0 0 0 0 D C B A
var mat = Sse2.LoadScalarVector128((ulong*)&m).AsInt16();
// pmaddwd: 0 0 DY+CX BY+AX
var dword_res = Sse2.MultiplyAddAdjacent(mat, vec);
// packssdw: 0 0 DY+CX BY+AX 0 0 DY+CX BY+AX
var rawres = Sse2.PackSignedSaturate(dword_res, dword_res);
Vec2 res;
*((int*)&res) = Sse2.ConvertToInt32(rawres.AsInt32());
return res;
}
Получившаяся сборка вполне разумна:
mov dword ptr [rsp+10h],ecx
mov qword ptr [rsp+18h],rdx
vmovd xmm0,dword ptr [rsp+18h]
vpshufd xmm0,xmm0,0
vmovq xmm1,mmword ptr [rsp+10h]
vpmaddwd xmm0,xmm1,xmm0
vpackssdw xmm0,xmm0,xmm0
vmovd eax,xmm0
mov dword ptr [rsp],eax
mov eax,dword ptr [rsp]
Но это не идеально. Аргументы функции m
и v
(и результат в конце) оба «отскакивают» через память… что, по общему признанию, именно то, что и сказал код C#. Это можно обойти, вручную объединив X
и Y
в int
с arithmeti c, а затем используя ConvertScalarToVector128Int32
, но тогда JIT, по-видимому, недостаточно умен, чтобы увидеть, что арифметическое c равно избыточный. Так что, похоже, нет хорошего решения. Надеюсь, что в какой-то момент JIT-оптимизатор сможет обнаруживать такие бессмысленные ситуации «отскока через память» и удалять их.
Другой момент - MultiplyAddAdjacent
частично потрачен впустую: он выполняет 8 продуктов, но только 4 - полезное вычисление, верхняя половина вектора - это просто нули. Если бы у вас было 2 вектора для умножения на одну и ту же матрицу 2x2, это можно было бы сделать с небольшими дополнительными затратами, намного меньшими, чем простой вызов вышеуказанной функции дважды.