这个第一版是由OP作为问题的编辑发布的,但它并不属于那里。出于纪念的目的,将其移动到社区维基答案中。
那个第一版在性能方面非常糟糕,是向量化的最差方式,内部循环中进行标量hsum,并使用insert_epi32
进行手动收集,甚至没有4x4转置。
更新:
哇喔!我终于搞清楚了。除了我的逻辑错误(感谢Peter Cordes的帮助),还有一个问题是_mm_mul_epi32()
并不像我想象的那样工作 - 我应该使用_mm_mullo_epi32()
代替!
我知道这不是最有效的代码,但它是为了让它正常工作而制作的 - 现在我可以开始优化它了。
(注意,不要使用这个,它非常非常慢)
void matmulSSE_inefficient(int mat1[N][N], int mat2[N][N], int result[N][N]) {
int i, j, k;
__m128i vA, vB, vR, vSum;
for(i = 0; i < N; ++i) {
for(j = 0; j < N; ++j) {
vR = _mm_setzero_si128();
for(k = 0; k < N; k += 4) {
vA = _mm_loadu_si128((__m128i*)&mat1[i][k]);
vB = _mm_insert_epi32(vB, mat2[k][j], 0);
vB = _mm_insert_epi32(vB, mat2[k + 1][j], 1);
vB = _mm_insert_epi32(vB, mat2[k + 2][j], 2);
vB = _mm_insert_epi32(vB, mat2[k + 3][j], 3);
vR = _mm_mullo_epi32(vA, vB);
vR = _mm_hadd_epi32(vR, vR);
vR = _mm_hadd_epi32(vR, vR);
result[i][j] += _mm_extract_epi32(vR, 0);
}
}
}
}
非常低效的代码已经由原作者结束
更新2: 将原帖中的示例转换为i-k-j循环顺序版本。需要额外的vR负载并将存储器移入内部循环,但是设置vA可以向上移动一个循环。结果更快。
void matmulSSE_v2(int mat1[N][N], int mat2[N][N], int result[N][N]) {
int i, j, k;
__m128i vA, vB, vR;
for(i = 0; i < N; ++i) {
for(k = 0; k < N; ++k) {
vA = _mm_set1_epi32(mat1[i][k]);
for(j = 0; j < N; j += 4) {
vB = _mm_loadu_si128((__m128i*)&mat2[k][j]);
vR = _mm_loadu_si128((__m128i*)&result[i][j]);
vR = _mm_add_epi32(vR, _mm_mullo_epi32(vA, vB));
_mm_storeu_si128((__m128i*)&result[i][j], vR);
}
}
}
}
这些假设N是向量宽度的倍数
如果不是这种情况,通常更容易仍然将数组存储填充到向量宽度的倍数,这样每行末尾都有填充,你可以使用简单的 j < N; j += 4
循环条件。你将想要跟踪实际的 N
大小,它与行跨度一起是 4 或 8 的倍数而分开存放。
否则,你需要一个类似于 j < N-3
; j += 4` 的循环条件,并进行标量清除以结束一行。
或者进行掩蔽操作或保持最后一个完整向量在寄存器中,这样就可以使用可能重叠行末的最终向量来执行 _mm_alignr_epi8
,并可能进行矢量存储。这在 AVX 或特别是 AVX512 掩蔽下更容易实现。
result[]
?如果没有,您应该先这样做!还要注意,在最内层循环中进行水平求和是可怕的。如果您在同一最内层循环中为result[i][j]
执行所有数学运算,则只需执行result = hsum(vR)
而不是+=
。其中hsum是一个水平求和函数,可移植到非MSVC(如果有必要)且比您编写的编译器产生的更好。请参见https://dev59.com/g2w05IYBdhLWcg3w11Qk,我的答案提到了整数hsums。 - Peter Cordes