我有一个程序几乎花费所有时间计算循环,如
for(int j = 0; j < BIGNUMBER; j++)
for(int i = 0; i < SMALLNUMBER; i++)
result += var[i] / sqrt((A[i].x-B[j].x)*(A[i].x-B[j].x)+(A[i].y-B[j].y)*(A[i].y-B[j].y)+(A[i].z-B[j].z)*(A[i].z-B[j].z));
1.0/sqrt(...)
计算两个向量A[i] = {A[i].x, A[i].y, A[i].z}
和B[j] = {B[j].x, B[j].y, B[j].z}
之间差异的范数的倒数,这也是循环中成本最高的部分。
有没有办法优化循环,即使有一些精度损失?
这里是非向量化循环步骤的汇编代码,每条指令的延迟更差。你清楚地看到反平方根是瓶颈:
movsd A(%rip), %xmm0 1
movsd A+8(%rip), %xmm2 1
subsd B(%rip), %xmm0 3
subsd B+8(%rip), %xmm2 3
movsd A+16(%rip), %xmm1 1
mulsd %xmm0, %xmm0 5
subsd B+16(%rip), %xmm1 3
mulsd %xmm2, %xmm2 5
mulsd %xmm1, %xmm1 5
addsd %xmm2, %xmm0 3
addsd %xmm1, %xmm0 3
movsd .LC0(%rip), %xmm1 1
unpcklpd %xmm0, %xmm0 1
cvtpd2ps %xmm0, %xmm0 4
unpcklps %xmm0, %xmm0 3
cvtps2pd %xmm0, %xmm0 2
sqrtsd %xmm0, %xmm0 58
divsd %xmm0, %xmm1 32
mulsd var(%rip), %xmm1 5
addsd result(%rip), %xmm1 3
cvttsd2si %xmm1, %eax 3
movsd %xmm1, result(%rip) 1
(顺便说一句,我不明白为什么会这样做unpcklpd cvtpd2ps unpcklps cvtps2pd
。)
答案 0 :(得分:3)
假设sqrt
是“瓶颈”很诱人,但如果你做this,你可以肯定地发现。
很可能是那个
(A[i].x-B[j].x)*(A[i].x-B[j].x)
+ (A[i].y-B[j].y)*(A[i].y-B[j].y)
+ (A[i].z-B[j].z)*(A[i].z-B[j].z)
是瓶颈。
如果您依赖编译器来优化所有这些索引操作,它可能会,但我想确定。作为第一个剪辑,我会在内循环中写这个:
// where Foo is the class of A[i] and B[j]
Foo& a = A[i];
Foo& b = B[j];
double dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
result += var[i] / sqrt( dx*dx + dy*dy + dz*dz );
编译器更容易优化。
然后,如果索引仍然是一个瓶颈,我会将Foo& b = B[j]
提升出内循环,然后执行指针而不是写A[i]
。
如果它到达样本显示内部for
语句本身(测试终止和递增)需要相当长的时间的点,我会将它展开一些。
答案 1 :(得分:3)
如果您可以使用AoSoA形式(xxyyzzxxyyzzxxyyzz ...)排列载体,则可以使用SSE或AVX(xxxxyyyyzzzz ...)非常有效地完成此操作。在下面的代码中,我假设SSE2具有vec_size = 2,但很容易将其更改为AVX。但是你的代码很可能是内存绑定而不是计算绑定,所以这对于适合L1缓存的小循环只会有用。使用单个浮点数也会更快,因为它的执行数量是翻牌数的两倍,而sqrt是少数几个实际上比浮点数慢的函数之一。
resultv = _mm_setzero_pd(0);
for(int j = 0; j < BIGNUMBER; j+=vec_size) {
bx = _mm_load_pd(&B[3*j+0*vec_size]);
by = _mm_load_pd(&B[3*j+1*vec_size]);
bz = _mm_load_pd(&B[3*j+2*vec_size]);
for(int i = 0; i < SMALLNUMBER; i+=vec_size) {
ax = _mm_load_pd(&A[3*i+0*vec_size]);
ay = _mm_load_pd(&A[3*i+1*vec_size]);
az = _mm_load_pd(&A[3*i+2*vec_size]);
dx = _mm_sub_pd(ax,bx);
dy = _mm_sub_pd(ay,by);
dz = _mm_sub_pd(az,bz);
mag2 = _mm_add_pd(_mm_add_pd(_mm_mul_pd(dx,dx),_mm_mul_pd(dy,dy)), _mm_mul_pd(dz,dz));
varv = _mm_load_pd(&var[i]);
resultv = _mm_add_pd(_mm_div_pd(varv, _mm_sqrt_pd(mag2)), resultv);
//resultv = _mm_add_pd(_mm_mul_pd(varv, _mm_rsqrt_pd(mag2)), resultv);
}
}
result = _mm_cvtsd_f64(_mm_hadd_pd(resultv,resultv));
答案 2 :(得分:2)
在评论中你说the bottleneck is still the computation of the inverse square root.
幸运的是,这是图形中出现的很多东西,并且有一种非常奇特的算法可以做到。有一篇关于它的wikipedia文章,quake中的实施和SO问题3。
答案 3 :(得分:1)
这是SIMD(SSE)指令的良好工作场所。如果编译器支持自动矢量化,请启用此选项(调整数据结构布局以获得实际增益)。如果它支持内在函数,您可以使用它们。
修改强>
上方的汇编代码不会利用向量化。它使用标量SSE指令。
您可以尝试帮助编译器 - 使浮点数(X,Y,Z,Dummy)的结构,而不是双重。 SSE矢量指令可以同时处理4个浮点数(或2个双打)
(我认为一些数学库已经包含了矢量规范的SIMD函数)
P.S。您可以将SSE标记添加到问题
答案 4 :(得分:0)
通过(1.0f/sqrt(float(...)))
强制执行单精度计算并使用#pragma GCC optimize ("Ofast")
函数,我能够获得rsqrtss
指令,速度更快(大约快2倍) )关于整个功能。它实际上打破了自动矢量化(可能是因为单精度和双精度的混合)。
汇编代码:
movsd 56(%rax), %xmm0
addq $120, %rax
movsd -72(%rax), %xmm2
subsd %xmm5, %xmm0
movsd -56(%rax), %xmm1
subsd %xmm4, %xmm2
subsd %xmm6, %xmm1
mulsd %xmm0, %xmm0
mulsd %xmm2, %xmm2
mulsd %xmm1, %xmm1
addsd %xmm2, %xmm0
addsd %xmm1, %xmm0
unpcklpd %xmm0, %xmm0
cvtpd2ps %xmm0, %xmm0
rsqrtss %xmm0, %xmm1
mulss %xmm1, %xmm0
mulss %xmm1, %xmm0
mulss %xmm7, %xmm1
addss %xmm8, %xmm0
mulss %xmm1, %xmm0
mulss -40(%rax), %xmm0
cmpq %rdx, %rax
unpcklps %xmm0, %xmm0
cvtps2pd %xmm0, %xmm0
addsd %xmm0, %xmm3
但我不了解最后的额外乘法。