AVX 中的矩阵向量乘法不成比例地比 SSE 中快
Matrix-vector-multiplication in AVX not proportionately faster than in SSE
我正在使用以下内容在SSE和AVX中编写矩阵向量乘法:
for(size_t i=0;i<M;i++) {
size_t index = i*N;
__m128 a, x, r1;
__m128 sum = _mm_setzero_ps();
for(size_t j=0;j<N;j+=4,index+=4) {
a = _mm_load_ps(&A[index]);
x = _mm_load_ps(&X[j]);
r1 = _mm_mul_ps(a,x);
sum = _mm_add_ps(r1,sum);
}
sum = _mm_hadd_ps(sum,sum);
sum = _mm_hadd_ps(sum,sum);
_mm_store_ss(&C[i],sum);
}
我对 AVX 使用了类似的方法,但是在最后,由于 AVX 没有等效的指令来_mm_store_ss()
,我使用了:
_mm_store_ss(&C[i],_mm256_castps256_ps128(sum));
SSE 代码使我比串行代码加速了 3.7。但是,AVX 代码比串行代码仅提高了 4.3 的加速比。
我知道将 SSE 与 AVX 一起使用可能会导致问题,但我使用 g++ 使用 -mavx' 标志编译它,这应该删除 SSE 操作码。
我也可以使用:_mm256_storeu_ps(&C[i],sum)
做同样的事情,但加速是相同的。
关于我还能做些什么来提高性能的任何见解?它是否与:performance_memory_bound有关,尽管我没有清楚地理解该线程上的答案。
另外,即使包含"immintrin.h"头文件,我也无法使用_mm_fmadd_ps()指令。我启用了FMA和AVX。
我建议你重新考虑你的算法。 请参阅讨论使用 SSE 进行高效的 4x4 矩阵向量乘法:水平加法和点积 - 有什么意义?
您正在做一个长点积,每次迭代使用_mm_hadd_ps
。 相反,您应该使用 SSE 一次执行四个点积(使用 AVX 执行八个点积),并且仅使用垂直运算符。
您需要加法、乘法和广播。 这一切都可以在SSE中完成,包括_mm_add_ps
、_mm_mul_ps
和_mm_shuffle_ps
(用于广播)。
如果您已经有了矩阵的转置,这真的很简单。
但是,无论您是否有转置,您都需要使您的代码对缓存更加友好。 为了解决这个问题,我建议对矩阵进行循环平铺。 请参阅此讨论 在C++中转置矩阵的最快方法是什么?了解如何进行循环平铺。
我会在尝试 SSE/AVX 之前先尝试让循环平铺正确。 我在矩阵乘法中获得的最大提升不是来自 SIMD 或线程,而是来自循环平铺。 我认为,如果您正确使用缓存,与SSE相比,AVX代码的性能也会更加线性。
请考虑此代码。我不熟悉INTEL版本,但这比DirectX中的XMMatrixMultiply更快。这不是关于每条指令完成多少数学运算,而是关于减少指令计数(只要您使用快速指令,此实现就是这样做的)。
// Perform a 4x4 matrix multiply by a 4x4 matrix
// Be sure to run in 64 bit mode and set right flags
// Properties, C/C++, Enable Enhanced Instruction, /arch:AVX
// Having MATRIX on a 32 byte bundry does help performance
struct MATRIX {
union {
float f[4][4];
__m128 m[4];
__m256 n[2];
};
}; MATRIX myMultiply(MATRIX M1, MATRIX M2) {
MATRIX mResult;
__m256 a0, a1, b0, b1;
__m256 c0, c1, c2, c3, c4, c5, c6, c7;
__m256 t0, t1, u0, u1;
t0 = M1.n[0]; // t0 = a00, a01, a02, a03, a10, a11, a12, a13
t1 = M1.n[1]; // t1 = a20, a21, a22, a23, a30, a31, a32, a33
u0 = M2.n[0]; // u0 = b00, b01, b02, b03, b10, b11, b12, b13
u1 = M2.n[1]; // u1 = b20, b21, b22, b23, b30, b31, b32, b33
a0 = _mm256_shuffle_ps(t0, t0, _MM_SHUFFLE(0, 0, 0, 0)); // a0 = a00, a00, a00, a00, a10, a10, a10, a10
a1 = _mm256_shuffle_ps(t1, t1, _MM_SHUFFLE(0, 0, 0, 0)); // a1 = a20, a20, a20, a20, a30, a30, a30, a30
b0 = _mm256_permute2f128_ps(u0, u0, 0x00); // b0 = b00, b01, b02, b03, b00, b01, b02, b03
c0 = _mm256_mul_ps(a0, b0); // c0 = a00*b00 a00*b01 a00*b02 a00*b03 a10*b00 a10*b01 a10*b02 a10*b03
c1 = _mm256_mul_ps(a1, b0); // c1 = a20*b00 a20*b01 a20*b02 a20*b03 a30*b00 a30*b01 a30*b02 a30*b03
a0 = _mm256_shuffle_ps(t0, t0, _MM_SHUFFLE(1, 1, 1, 1)); // a0 = a01, a01, a01, a01, a11, a11, a11, a11
a1 = _mm256_shuffle_ps(t1, t1, _MM_SHUFFLE(1, 1, 1, 1)); // a1 = a21, a21, a21, a21, a31, a31, a31, a31
b0 = _mm256_permute2f128_ps(u0, u0, 0x11); // b0 = b10, b11, b12, b13, b10, b11, b12, b13
c2 = _mm256_mul_ps(a0, b0); // c2 = a01*b10 a01*b11 a01*b12 a01*b13 a11*b10 a11*b11 a11*b12 a11*b13
c3 = _mm256_mul_ps(a1, b0); // c3 = a21*b10 a21*b11 a21*b12 a21*b13 a31*b10 a31*b11 a31*b12 a31*b13
a0 = _mm256_shuffle_ps(t0, t0, _MM_SHUFFLE(2, 2, 2, 2)); // a0 = a02, a02, a02, a02, a12, a12, a12, a12
a1 = _mm256_shuffle_ps(t1, t1, _MM_SHUFFLE(2, 2, 2, 2)); // a1 = a22, a22, a22, a22, a32, a32, a32, a32
b1 = _mm256_permute2f128_ps(u1, u1, 0x00); // b0 = b20, b21, b22, b23, b20, b21, b22, b23
c4 = _mm256_mul_ps(a0, b1); // c4 = a02*b20 a02*b21 a02*b22 a02*b23 a12*b20 a12*b21 a12*b22 a12*b23
c5 = _mm256_mul_ps(a1, b1); // c5 = a22*b20 a22*b21 a22*b22 a22*b23 a32*b20 a32*b21 a32*b22 a32*b23
a0 = _mm256_shuffle_ps(t0, t0, _MM_SHUFFLE(3, 3, 3, 3)); // a0 = a03, a03, a03, a03, a13, a13, a13, a13
a1 = _mm256_shuffle_ps(t1, t1, _MM_SHUFFLE(3, 3, 3, 3)); // a1 = a23, a23, a23, a23, a33, a33, a33, a33
b1 = _mm256_permute2f128_ps(u1, u1, 0x11); // b0 = b30, b31, b32, b33, b30, b31, b32, b33
c6 = _mm256_mul_ps(a0, b1); // c6 = a03*b30 a03*b31 a03*b32 a03*b33 a13*b30 a13*b31 a13*b32 a13*b33
c7 = _mm256_mul_ps(a1, b1); // c7 = a23*b30 a23*b31 a23*b32 a23*b33 a33*b30 a33*b31 a33*b32 a33*b33
c0 = _mm256_add_ps(c0, c2); // c0 = c0 + c2 (two terms, first two rows)
c4 = _mm256_add_ps(c4, c6); // c4 = c4 + c6 (the other two terms, first two rows)
c1 = _mm256_add_ps(c1, c3); // c1 = c1 + c3 (two terms, second two rows)
c5 = _mm256_add_ps(c5, c7); // c5 = c5 + c7 (the other two terms, second two rose)
// Finally complete addition of all four terms and return the results
mResult.n[0] = _mm256_add_ps(c0, c4); // n0 = a00*b00+a01*b10+a02*b20+a03*b30 a00*b01+a01*b11+a02*b21+a03*b31 a00*b02+a01*b12+a02*b22+a03*b32 a00*b03+a01*b13+a02*b23+a03*b33
// a10*b00+a11*b10+a12*b20+a13*b30 a10*b01+a11*b11+a12*b21+a13*b31 a10*b02+a11*b12+a12*b22+a13*b32 a10*b03+a11*b13+a12*b23+a13*b33
mResult.n[1] = _mm256_add_ps(c1, c5); // n1 = a20*b00+a21*b10+a22*b20+a23*b30 a20*b01+a21*b11+a22*b21+a23*b31 a20*b02+a21*b12+a22*b22+a23*b32 a20*b03+a21*b13+a22*b23+a23*b33
// a30*b00+a31*b10+a32*b20+a33*b30 a30*b01+a31*b11+a32*b21+a33*b31 a30*b02+a31*b12+a32*b22+a33*b32 a30*b03+a31*b13+a32*b23+a33*b33
return mResult;
}
正如有人已经建议的那样,添加-趣味循环
奇怪的是,这不是默认设置的。
使用 __restrict 定义任何浮点指针。将 const 用于常量数组引用。我不知道编译器是否足够聪明,可以识别循环中的 3 个中间值不需要在迭代之间保持活动状态。我只会删除这 3 个变量,或者至少使它们在循环中本地(a、x、r1)。可以声明索引,其中声明 j 以使其更本地化。确保将 M 和 N 声明为 const,如果它们的值是编译时常量,则让编译器查看它们。
- 计算缩放多边形的比例,得到给定的多边形面积
- OpenGL 和 GLM 矩阵无法正确扩展,总是按比例缩小
- C++ SSE 内部函数:将结果存储在变量中
- 使用 assimp 获取纹理比例
- Constexpr and SSE intrinsics
- 如何使用SSE将__m128i注册乘以浮点因子?
- C++如何判断互斥体在阻塞其他线程时是否被单个线程不成比例地占用
- 根据比例更改 QProgressbar 块的颜色
- 使用SSE内部函数复制少量数据时出现问题
- 可以在 macOS 上启用的最低支持的 SSE 标志是什么?
- SSE 标志应该如何与现代 CMake 一起添加?
- 快速 SSE 射线 - 4 三角形交叉点
- 使用 SSE 以最快的速度缩小 8 位灰度图像
- 如何用SSE优化矩阵3乘3乘法与点?
- 使用双精度运算的快速 SSE 低精度指数
- 手动矢量化/SSE 用于 C++ 中的复杂问题
- 单精度矩阵运算的特征性能 AVX 与 SSE 没有区别?
- 编写 std::copysign 的可移植 SSE/AVX 版本
- SSE 内联汇编和可能的 g++ 优化错误
- AVX 中的矩阵向量乘法不成比例地比 SSE 中快