我正在尝试使用SSE4 dot产品改进此代码,但我很难找到解决方案。此函数获取参数qi和tj,其中包含每个80个单元格的浮点数组,然后计算点积。返回值是具有四个点积的向量。所以我想要做的就是并行计算20个值的四个点积。
您是否知道如何改进此代码?
inline __m128 ScalarProd20Vec(__m128* qi, __m128* tj)
{
__m128 res=_mm_add_ps(_mm_mul_ps(tj[0],qi[0]),_mm_mul_ps(tj[1],qi[1]));
res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[2],qi[2]),_mm_mul_ps(tj[3],qi[3])));
res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[4],qi[4]),_mm_mul_ps(tj[5],qi[5])));
res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[6],qi[6]),_mm_mul_ps(tj[7],qi[7])));
res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[8],qi[8]),_mm_mul_ps(tj[9],qi[9])));
res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[10],qi[10]),_mm_mul_ps(tj[11],qi[11])));
res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[12],qi[12]),_mm_mul_ps(tj[13],qi[13])));
res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[14],qi[14]),_mm_mul_ps(tj[15],qi[15])));
res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[16],qi[16]),_mm_mul_ps(tj[17],qi[17])));
res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[18],qi[18]),_mm_mul_ps(tj[19],qi[19])));
return res;
}
答案 0 :(得分:9)
在我看过的数百个SSE示例中,您的代码是从一开始就已经处于良好状态的少数代码之一。您不需要SSE4 dot-product指令。 (你可以做得更好!)
但是,有一件事你可以尝试:(我说试试,因为我还没有计时。)
目前,您在res
上拥有数据依赖关系链。今天大多数机器上的矢量添加是3-4个循环。因此,您的代码将至少运行30个周期,因为您有:
(10 additions on critical path) * (3 cycles addps latency) = 30 cycles
您可以做的是按如下方式对res
变量进行节点拆分:
__m128 res0 = _mm_add_ps(_mm_mul_ps(tj[ 0],qi[ 0]),_mm_mul_ps(tj[ 1],qi[ 1]));
__m128 res1 = _mm_add_ps(_mm_mul_ps(tj[ 2],qi[ 2]),_mm_mul_ps(tj[ 3],qi[ 3]));
res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[ 4],qi[ 4]),_mm_mul_ps(tj[ 5],qi[ 5])));
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[ 6],qi[ 6]),_mm_mul_ps(tj[ 7],qi[ 7])));
res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[ 8],qi[ 8]),_mm_mul_ps(tj[ 9],qi[ 9])));
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[10],qi[10]),_mm_mul_ps(tj[11],qi[11])));
res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[12],qi[12]),_mm_mul_ps(tj[13],qi[13])));
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[14],qi[14]),_mm_mul_ps(tj[15],qi[15])));
res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[16],qi[16]),_mm_mul_ps(tj[17],qi[17])));
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[18],qi[18]),_mm_mul_ps(tj[19],qi[19])));
return _mm_add_ps(res0,res1);
这几乎将你的关键路径减少了一半。请注意,由于浮点非关联性,这种优化对于编译器来说是非法的。
这是使用4路节点分割和AMD FMA4指令的替代版本。如果您不能使用融合乘法,请随意将它们分开。它可能仍然比上面的第一个版本更好。
__m128 res0 = _mm_mul_ps(tj[ 0],qi[ 0]);
__m128 res1 = _mm_mul_ps(tj[ 1],qi[ 1]);
__m128 res2 = _mm_mul_ps(tj[ 2],qi[ 2]);
__m128 res3 = _mm_mul_ps(tj[ 3],qi[ 3]);
res0 = _mm_macc_ps(tj[ 4],qi[ 4],res0);
res1 = _mm_macc_ps(tj[ 5],qi[ 5],res1);
res2 = _mm_macc_ps(tj[ 6],qi[ 6],res2);
res3 = _mm_macc_ps(tj[ 7],qi[ 7],res3);
res0 = _mm_macc_ps(tj[ 8],qi[ 8],res0);
res1 = _mm_macc_ps(tj[ 9],qi[ 9],res1);
res2 = _mm_macc_ps(tj[10],qi[10],res2);
res3 = _mm_macc_ps(tj[11],qi[11],res3);
res0 = _mm_macc_ps(tj[12],qi[12],res0);
res1 = _mm_macc_ps(tj[13],qi[13],res1);
res2 = _mm_macc_ps(tj[14],qi[14],res2);
res3 = _mm_macc_ps(tj[15],qi[15],res3);
res0 = _mm_macc_ps(tj[16],qi[16],res0);
res1 = _mm_macc_ps(tj[17],qi[17],res1);
res2 = _mm_macc_ps(tj[18],qi[18],res2);
res3 = _mm_macc_ps(tj[19],qi[19],res3);
res0 = _mm_add_ps(res0,res1);
res2 = _mm_add_ps(res2,res3);
return _mm_add_ps(res0,res2);
答案 1 :(得分:3)
首先,您可以做的最重要的优化是确保您的编译器已启用其所有优化设置。
编译器很聪明,所以如果把它写成循环,很可能会将它展开:
__128 res = _mm_setzero();
for (int i = 0; i < 10; i++) {
res = _mm_add_ps(res, _mm_add_ps(_mm_mul_ps(tj[2*i], qi[2*i]), _mm_mul_ps(tj[2*i+1], qi[2*i+1])));
}
return res;
(使用GCC,您需要传递-funroll-loops
,然后它会将其展开以一次进行5次迭代。)
如果循环版本较慢,你也可以定义一个宏并手动展开它,例如:
__128 res = _mm_setzero();
#define STEP(i) res = _mm_add_ps(res, _mm_add_ps(_mm_mul_ps(tj[2*i], qi[2*i]), _mm_mul_ps(tj[2*i+1], qi[2*i+1])))
STEP(0); STEP(1); STEP(2); STEP(3); STEP(4);
STEP(5); STEP(6); STEP(7); STEP(8); STEP(9);
#undef STEP
return res;
你甚至可以从0到20运行循环(或者与宏版本一样),即:
__128 res = _mm_setzero();
for (int i = 0; i < 20; i++) {
res = _mm_add_ps(res, _mm_mul_ps(tj[i], qi[i]));
}
return res;
(使用GCC和-funroll-loops
展开,一次进行10次迭代,即与上面的两次循环相同。)
答案 2 :(得分:2)
您的数据未按适当格式存储在内存中,以用于专门的SSE4点积指令(dpps
)。这些指令期望单个向量的维度相邻,如下所示:
| dim0 | dim1 | dim2 | ... | dim19 |
而您的数据似乎互相交错:
| v0-dim0 | v1-dim0 | v2-dim0 | v3-dim0 | v0-dim1 | ...
您当前的一般方法似乎是合适的 - 您可以通过重新排序指令来改进事物,使得乘法的结果在生成后不会立即使用,但实际上编译器应该能够解决这个问题。它自己的。