带SIMD的C#中的2x2矩阵向量积

带SIMD的C#中的2x2矩阵向量积,c#,simd,C#,Simd,我正在做一些事情,我想用不同的二维short值向量乘以相同的2x2short值矩阵,每秒多次,在这种情况下,性能很重要。现在,我只是用简单的方法写下矩阵乘法。我查阅了C#的SIMD功能,发现无法生成这种类型的2x2矩阵。所以我试着用System.Numerics.Vectors中的Vector结构来实现它。但构造函数希望向量中至少有4个元素。我可以解决这个问题,并使它与四维向量一起工作,但我想知道是否有一种方法可以更容易地完成我想做的事情:将一个2x2矩阵与一个二维向量相乘,然后用SIMD生成一

我正在做一些事情,我想用不同的二维
short
值向量乘以相同的2x2
short
值矩阵,每秒多次,在这种情况下,性能很重要。现在,我只是用简单的方法写下矩阵乘法。我查阅了C#的SIMD功能,发现无法生成这种类型的2x2矩阵。所以我试着用
System.Numerics.Vectors
中的
Vector
结构来实现它。但构造函数希望向量中至少有4个元素。我可以解决这个问题,并使它与四维向量一起工作,但我想知道是否有一种方法可以更容易地完成我想做的事情:将一个2x2矩阵与一个二维向量相乘,然后用SIMD生成一个新的二维向量。

使用
System.Runtime.Intrinsics.X86
,可以用来完成繁重的工作,用一些洗牌等来排列数据。例如:

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
,然后使用
ConvertScalarToVector128Int32
来解决,但JIT显然不够聪明,无法看出算术是多余的。因此,似乎没有好的解决办法。希望JIT优化器在某个时候能够检测到这种毫无意义的“内存反弹”情况并将其删除


另一点是,
MultiplyAddAdjacent
部分被浪费了:它有8个乘积,但只有4个是有用的计算,向量的上半部分只是零。如果有两个向量与同一个2x2矩阵相乘,则只需花费很少的额外成本即可完成,这比简单地调用上述函数两次要少得多。

使用
System.Runtime.Intrinsics.X86
,可用于完成繁重的工作,并进行一些洗牌等来排列数据。例如:

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
,然后使用
ConvertScalarToVector128Int32
来解决,但JIT显然不够聪明,无法看出算术是多余的。因此,似乎没有好的解决办法。希望JIT优化器在某个时候能够检测到这种毫无意义的“内存反弹”情况并将其删除


另一点是,
MultiplyAddAdjacent
部分被浪费了:它有8个乘积,但只有4个是有用的计算,向量的上半部分只是零。如果有两个向量与同一个2x2矩阵相乘,则只需花费很少的额外成本即可完成,这比简单地调用上述函数两次要少得多。

您是否能够使用
System.Runtime.Intrinsics.X86
中的SIMD instrinsics?@harold这些函数在任何系统上都能工作?从微软的文档来看,它们似乎只能在英特尔处理器上工作,如果这是你的意思的话,它可以在英特尔和AMD处理器上工作。但不是ARM。您能从
System.Runtime.Intrinsics.X86
使用SIMD指令集吗?@harold这些指令集在任何系统上都能工作吗?从微软的文档来看,它们似乎只能在英特尔处理器上工作,如果这是你的意思的话,它可以在英特尔和AMD处理器上工作。但不是ARM。同样令人失望的是,启用AVX的编译没有使用
vbroadcastss
而不是
vmovd
+
vpshufd
@PeterCordes是的,可以使用
vec=AVX.BroadcastScalarToVector128((float*)&v.AsInt16()手动完成
(仍然会发生内存跳转)同样令人失望的是,启用AVX的编译没有使用
vbroadcastss
而不是
vmovd
+
vpshufd
@PeterCordes是的,可以使用
vec=AVX.BroadcastScalarToVector128((float*)&v.AsInt16()手动完成(仍会发生内存跳转)