【问题标题】:Fast inverse norm function快速反范数函数
【发布时间】:2014-05-06 10:56:15
【问题描述】:

我有一个程序几乎把他所有的时间都花在计算循环上,比如

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

【问题讨论】:

  • 你的问题应该移到math.stackexchange.com
  • @fluminis,你舒尔吗?
  • 您确定1./sqrt(x) 是瓶颈吗?如果是这样,你能避免吗? (其余的计算可以通过使用一些线性代数来加速,但那部分不能。)
  • 好吧,假设我可以添加这些延迟,它们是 146 个周期,所以 33% 在 sqrtsd,22% 在 divsd,45% 在其他所有东西中,所以可能在sqrtsd 之外仍有改进的空间。 “瓶颈”这个词的问题在于它会让你倾向于只看一个地方,并且可能仅仅因为它是漫射的而忽略更大的东西。
  • 很难判断您的数据类型是什么,但从生成的代码看来,您正在混合使用浮点数和双精度数。如果您可以将所有数据、计算和函数调用限制为单精度浮点数,那么您应该会获得更好的性能。

标签: math optimization assembly sse micro-optimization


【解决方案1】:

如果您可以以 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));

【讨论】:

  • 不幸的是,目标 CPU 没有 AVX。
  • @Kyle_the_hacker,没关系,我发布的代码是针对 SSE2 的。与您的代码相比,我的代码的主要优点是您的代码每次迭代都求平方根。我的代码仅按 vec_size 执行。如果您将 float 与 SSE2 一起使用,则意味着每四次迭代一次。
  • 是的,我明白了。这只是一个评论。 ;) 我将尝试在我的程序中实现 AoSoA。谢谢。
  • @Kyle_the_hacker,将我发布的代码转换为浮动代码应该很容易。将所有 pd 扩展更改为 ps 并设置 vec_size=4。最后一行 result = 必须以不同的方式完成。
  • 也许在外循环中通过 'i' 迭代效率更高(var[i] 可以加载一次,内循环更长)。优化的另一种可能性是使用快速但不精确的 _mm_rsqrt_ps 指令,然后添加一到两次 Newton-Raphson 迭代(取决于所需的精度)。
【解决方案2】:

很容易假设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&amp; b = B[j] 提升出内部循环,并单步执行指针而不是编写A[i]。 如果样本显示内部 for 语句本身(测试终止和递增)需要相当长的时间,我会展开一些。

【讨论】:

    【解决方案3】:

    您在评论中说the bottleneck is still the computation of the inverse square root. 幸运的是,这在图形中出现了很多,并且有一个非常奇特的算法可以做到这一点。有一篇关于它的 wikipedia 文章,quake 中的一个实现和一个 SO 问题 3

    【讨论】:

    • 我知道 Quake 的实现。在实际的 CPU 上,它与正常的 1.0f/sqrt(...) 函数一样快。这不再是优化。
    • 那么您的编译器正在使用 SIMD 计算 sqrt。如果瓶颈是平方根,我认为你不会比这快得多。也许耶普! (yeppp.info/benchmarks.html) 或 MKL 可以帮助加快实现速度,但不要指望会有很大的变化。
    • 看起来像耶!没有任何平方根的实现。我无法访问 MKL。
    • “该算法使用牛顿法的独特一阶近似法生成相当准确的结果;但是,它比在 1999 年发布的 x86 处理器上使用 SSE 指令 rsqrtss 慢得多且精度更低。”跨度>
    【解决方案4】:

    这是 SIMD (SSE) 指令的良好工作场所。如果您的编译器支持自动矢量化,请启用此选项(调整数据结构布局以获得实际增益)。如果它支持内在函数,您可以使用它们。

    编辑
    上面的汇编代码没有利用矢量化。它使用标量 SSE 指令。 您可能会尝试帮助编译器 - 制作(X,Y,Z,Dummy)浮点结构,而不是双精度。 SSE 向量指令可以同时处理 4 个浮点数(或 2 个双精度数)。
    (我认为一些数学库已经包含向量范数的 SIMD 函数)

    附:您可以在问题中添加 SSE 标签

    【讨论】:

    • 它已经做了向量化,但瓶颈仍然是逆平方根的计算。
    • 生成的汇编代码是否包含 SSE RCPSS/RSQRTSS(或打包变体)指令或 x87 SQRT?
    • 我已经尝试计算 temp = (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) 然后 invSqrt = _mm_rsqrt_ss(_mm_load_ss(&amp;temp))[0] 这破坏了自动矢量化,扩展了 asm 代码,最终只快了一点点。
    • @Kyle_the_hacker 尝试在多个阶段进行可能有助于更容易矢量化。这意味着首先计算分母并存储在一个单独的数组中,然后在另一个 for 循环中对该数组进行逆平方根,并将 val[i] 与逆平方根值相乘
    【解决方案5】:

    通过通过(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
    

    但我不明白最后的附加乘法。

    【讨论】:

    • 确保rsqrtss 对您来说足够精确。我过去使用过它,它的结果是 too coarse in my case 。您可能需要对它进行一次牛顿方法的迭代。如果它足够好,也许你应该考虑使用单浮点。
    • 是的,我知道它用作查找表并且非常近似。我会看看牛顿步骤是否会对结果产生重大影响,但应该没问题。另一个问题是:AB 来自程序的其他部分,我对它没有太多控制;我必须检查在飞行中将它们转换为单精度 AoSoA 是否有助于提高速度。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多