C SSE双打,不值得吗?
我读了一些关于使用SSE内部函数的书,并尝试用双精度实现四元数旋转。下面是我写的普通函数和SSE函数C SSE双打,不值得吗?,c,performance,double,sse,quaternions,C,Performance,Double,Sse,Quaternions,我读了一些关于使用SSE内部函数的书,并尝试用双精度实现四元数旋转。下面是我写的普通函数和SSE函数 void quat_rot(quat_t a, REAL* restrict b){ /////////////////////////////////////////// // Multiply vector b by quaternion a // /////////////////////////////////////////// REAL cr
void quat_rot(quat_t a, REAL* restrict b){
///////////////////////////////////////////
// Multiply vector b by quaternion a //
///////////////////////////////////////////
REAL cross_temp[3],result[3];
cross_temp[0]=a.el[2]*b[2]-a.el[3]*b[1]+a.el[0]*b[0];
cross_temp[1]=a.el[3]*b[0]-a.el[1]*b[2]+a.el[0]*b[1];
cross_temp[2]=a.el[1]*b[1]-a.el[2]*b[0]+a.el[0]*b[2];
result[0]=b[0]+2.0*(a.el[2]*cross_temp[2]-a.el[3]*cross_temp[1]);
result[1]=b[1]+2.0*(a.el[3]*cross_temp[0]-a.el[1]*cross_temp[2]);
result[2]=b[2]+2.0*(a.el[1]*cross_temp[1]-a.el[2]*cross_temp[0]);
b[0]=result[0];
b[1]=result[1];
b[2]=result[2];
}
与苏格兰和南方能源公司
inline void cross_p(__m128d *a, __m128d *b, __m128d *c){
const __m128d SIGN_NP = _mm_set_pd(0.0, -0.0);
__m128d l1 = _mm_mul_pd( _mm_unpacklo_pd(a[1], a[1]), b[0] );
__m128d l2 = _mm_mul_pd( _mm_unpacklo_pd(b[1], b[1]), a[0] );
__m128d m1 = _mm_sub_pd(l1, l2);
m1 = _mm_shuffle_pd(m1, m1, 1);
m1 = _mm_xor_pd(m1, SIGN_NP);
l1 = _mm_mul_pd( a[0], _mm_shuffle_pd(b[0], b[0], 1) );
__m128d m2 = _mm_sub_sd(l1, _mm_unpackhi_pd(l1, l1));
c[0] = m1;
c[1] = m2;
}
void quat_rotSSE(quat_t a, REAL* restrict b){
///////////////////////////////////////////
// Multiply vector b by quaternion a //
///////////////////////////////////////////
__m128d axb[2];
__m128d aa[2];
aa[0] = _mm_load_pd(a.el+1);
aa[1] = _mm_load_sd(a.el+3);
__m128d bb[2];
bb[0] = _mm_load_pd(b);
bb[1] = _mm_load_sd(b+2);
cross_p(aa, bb, axb);
__m128d w = _mm_set1_pd(a.el[0]);
axb[0] = _mm_add_pd(axb[0], _mm_mul_pd(w, bb[0]));
axb[1] = _mm_add_sd(axb[1], _mm_mul_sd(w, bb[1]));
cross_p(aa, axb, axb);
_mm_store_pd(b, _mm_add_pd(bb[0], _mm_add_pd(axb[0], axb[0])));
_mm_store_sd(b+2, _mm_add_pd(bb[1], _mm_add_sd(axb[1], axb[1])));
}
旋转基本上是使用函数完成的
然后我运行下面的测试来检查每个函数执行一组旋转需要多少时间
int main(int argc, char *argv[]){
REAL a[] __attribute__ ((aligned(16))) = {0.2, 1.3, 2.6};
quat_t q = {{0.1, 0.7, -0.3, -3.2}};
REAL sum = 0.0;
for(int i = 0; i < 4; i++) sum += q.el[i] * q.el[i];
sum = sqrt(sum);
for(int i = 0; i < 4; i++) q.el[i] /= sum;
int N = 1000000000;
for(int i = 0; i < N; i++){
quat_rotSSE(q, a);
}
printf("rot = ");
for(int i = 0; i < 3; i++) printf("%f, ", a[i]);
printf("\n");
return 0;
}
我使用GCC4.6.3和-O3-std=c99-msse3编译
使用unix时间
,正常功能的计时为18.841s,SSE功能的计时为21.689s
我是否遗漏了什么,为什么SSE的实施速度比正常的慢15%?在哪些情况下,SSE实现双精度会更快
编辑:根据评论的建议,我尝试了几种方法
- -O1选项给出了非常相似的结果
- 尝试在
函数上使用cross\u p
,并添加了一个uu m128d来保存第二个叉积。这在生产的组件中没有区别restrict
- 据我所知,为正常函数生成的程序集只包含标量指令,除了一些
movapd
编辑:添加到生成的程序集的链接
一些想法可能会使您的代码得到充分优化
- 您的函数应该是内联的
- 您应该将
规范添加到restrict
,以避免 编译器多次重新加载参数cross\p
- 如果这样做,则必须引入第四个
变量,该变量将接收第二次调用\uuum128d
的结果cross\p
array[i]=(array[i]*K+L)/M+N代码>对于每个元素,SSE/SIMD将有所帮助
如果在大量元素上没有执行相同的操作,那么SSE就没有帮助。例如,如果您有一个double,需要执行foo=(foo*K+L)/M+N代码>那么SSE/SIMD就帮不上忙了
基本上,SSE是这项工作的错误工具。您需要将工作转换为SSE是正确工具的工作。例如,而不是
将一个向量乘以一个四元数;尝试将1000个向量的数组乘以一个四元数,或者将1000个向量的数组乘以1000个四元数的数组
编辑:在此处添加以下所有内容强>
请注意,这通常意味着修改数据结构以适应需要。例如,与其拥有一个结构数组,不如拥有一个数组结构
例如,假设您的代码使用四元数数组,如下所示:
for(i = 0; i < quaternionCount; i++) {
cross_temp[i][0] = a[i][2] * b[i][2] - a[i][3] * b[i][1] + a[i][0] * b[i][0];
cross_temp[i][1] = a[i][3] * b[i][0] - a[i][1] * b[i][2] + a[i][0] * b[i][1];
cross_temp[i][2] = a[i][1] * b[i][1] - a[i][2] * b[i][0] + a[i][0] * b[i][2];
b[i][0] = b[i][0] + 2.0 * (a[i][2] * cross_temp[i][2] - a[i][3] * cross_temp[i][1]);
b[i][1] = b[i][1] + 2.0 * (a[i][3] * cross_temp[i][0] - a[i][1] * cross_temp[i][2]);
b[i][2] = b[i][2] + 2.0 * (a[i][1] * cross_temp[i][1] - a[i][2] * cross_temp[i][0]);
}
现在重新订购:
cross_temp[0][i] = a[2][i] * b[2][i];
cross_temp[0][i+1] = a[2][i+1] * b[2][i+1];
cross_temp[0][i] -= a[3][i] * b[1][i];
cross_temp[0][i+1] -= a[3][i+1] * b[1][i+1];
cross_temp[0][i] += a[0][i] * b[0][i];
cross_temp[0][i+1] += a[0][i+1] * b[0][i+1];
完成所有这些之后,考虑转换为SSE。前两行代码是一次加载(将a[2][i]
和a[2][i+1]
加载到SSE寄存器),然后是一次乘法(而不是两次单独加载和两次单独乘法)。这6行可能会变成(伪代码):
这里的每一行伪代码都是一条SSE指令/内部指令;并且每个SSE指令/内部指令并行执行两个操作
如果每条指令并行执行两个操作,那么(理论上)它的速度可能是原始“每条指令一个操作”代码的两倍。现在的编译器非常擅长优化,可能会将原始函数矢量化。您可以查看生成的程序集,以了解编译器对原始函数的作用。此外,在没有优化的情况下进行一次基准测试,以获得适当的基线。对于现代CPU,例如Core i7,您无论如何都有两个标量FPU,因此在大多数情况下,双向SIMD(这是SSE上的Double)不会是一个胜利。看看我的回答,SSE在矩阵乘法和到矩阵转换方面可以帮助很大。请注意,存在不同的四元数乘法算法,乘法次数较少,加法次数较多。
for(i = 0; i < quaternionCount; i += 2) {
cross_temp[0][i] = a[2][i] * b[2][i] - a[3][i] * b[1][i] + a[0][i] * b[0][i];
cross_temp[0][i+1] = a[2][i+1] * b[2][i+1] - a[3][i+1] * b[1][i+1] + a[0][i+1] * b[0][i+1];
cross_temp[1][i] = a[3][i] * b[0][i] - a[1][i] * b[2][i] + a[0][i] * b[1][i];
cross_temp[1][i+1] = a[3][i+1] * b[0][i+1] - a[1][i+1] * b[2][i+1] + a[0][i+1] * b[1][i+1];
cross_temp[2][i] = a[1][i] * b[1][i] - a[2][i] * b[0][i] + a[0][i] * b[2][i];
cross_temp[2][i+1] = a[1][i+1] * b[1][i+1] - a[2][i+1] * b[0][i+1] + a[0][i+1] * b[2][i+1];
b[0][i] = b[0][i] + 2.0 * (a[2][i] * cross_temp[2][i] - a[3][i] * cross_temp[1][i]);
b[0][i+1] = b[0][i+1] + 2.0 * (a[2][i+1] * cross_temp[2][i+1] - a[3][i+1] * cross_temp[1][i+1]);
b[1][i] = b[1][i] + 2.0 * (a[3][i] * cross_temp[0][i] - a[1][i] * cross_temp[2][i]);
b[1][i+1] = b[1][i+1] + 2.0 * (a[3][i+1] * cross_temp[0][i+1] - a[1][i+1] * cross_temp[2][i+1]);
b[2][i] = b[2][i] + 2.0 * (a[1][i] * cross_temp[1][i] - a[2][i] * cross_temp[0][i]);
b[2][i+1] = b[2][i+1] + 2.0 * (a[1][i+1] * cross_temp[1][i+1] - a[2][i+1] * cross_temp[0][i+1]);
}
cross_temp[0][i] = a[2][i] * b[2][i];
cross_temp[0][i] -= a[3][i] * b[1][i];
cross_temp[0][i] += a[0][i] * b[0][i];
cross_temp[0][i+1] = a[2][i+1] * b[2][i+1];
cross_temp[0][i+1] -= a[3][i+1] * b[1][i+1];
cross_temp[0][i+1] += a[0][i+1] * b[0][i+1];
cross_temp[0][i] = a[2][i] * b[2][i];
cross_temp[0][i+1] = a[2][i+1] * b[2][i+1];
cross_temp[0][i] -= a[3][i] * b[1][i];
cross_temp[0][i+1] -= a[3][i+1] * b[1][i+1];
cross_temp[0][i] += a[0][i] * b[0][i];
cross_temp[0][i+1] += a[0][i+1] * b[0][i+1];
load SSE_register1 with both a[2][i] and a[2][i+1]
multiply SSE_register1 with both b[2][i] and b[2][i+1]
load SSE_register2 with both a[3][i] and a[3][i+1]
multiply SSE_register2 with both b[1][i] and b[1][i+1]
load SSE_register2 with both a[0][i] and a[0][i+1]
multiply SSE_register2 with both b[0][i] and b[0][i+1]
SE_register1 = SE_register1 - SE_register2
SE_register1 = SE_register1 + SE_register3