快速反范数函数

时间:2014-05-06 10:56:15

标签: math optimization assembly sse micro-optimization

我有一个程序几乎花费所有时间计算循环,如

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。)

5 个答案:

答案 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

但我不了解最后的额外乘法。