计算距离平方的最快方法

时间:2011-11-10 10:19:51

标签: c optimization simd

我的代码在很大程度上依赖于计算3D空间中两点之间的距离。 为了避免昂贵的平方根,我使用整个平方距离。 但它仍然占用了计算时间的很大一部分,我想用更快的东西替换我的简单函数。 我现在有:

double distance_squared(double *a, double *b)
{
  double dx = a[0] - b[0];
  double dy = a[1] - b[1];
  double dz = a[2] - b[2];

  return dx*dx + dy*dy + dz*dz;
}

我也尝试使用宏来避免函数调用,但它没有多大帮助。

#define DISTANCE_SQUARED(a, b) ((a)[0]-(b)[0])*((a)[0]-(b)[0]) + ((a)[1]-(b)[1])*((a)[1]-(b)[1]) + ((a)[2]-(b)[2])*((a)[2]-(b)[2])

我考虑过使用SIMD指令,但找不到一个好的示例或完整的指令列表(理想情况下是一些乘法+加两个向量)。

GPU不是一个选项,因为每个函数调用只知道一组点。

计算距离平方的最快方法是什么?

7 个答案:

答案 0 :(得分:11)

一个优秀的编译器将优化您所能管理的内容。如果一个好的编译器认为它们将是有益的,它将使用SIMD指令。确保为编译器打开所有这些可能的优化。不幸的是,尺寸为3的向量与SIMD单元不相容。

我怀疑你只需要接受编译器生成的代码可能非常接近最优并且不会产生显着的收益。

答案 1 :(得分:8)

第一个显而易见的事情是使用restrict关键字。

就像现在一样,ab是可混淆的(因此,从编译器的角度来看,假设最坏的情况别名)。没有编译器会自动对其进行矢量化,因为这样做是错误的。

更糟糕的是,编译器不仅没有矢量化这样的循环,如果你也存储(幸运的是不在你的例子中),每次必须重新加载值。始终要清楚别名,因为它会极大地影响编译器。

接下来,如果你可以接受它,使用float代替double并填充4个浮点数即使一个未使用,这对于大多数CPU来说是一个更“自然”的数据布局(这在某种程度上是特定于平台的,但对于大多数平台来说,4个浮点数是一个很好的猜测 - 在“典型”CPU上,3个双打,即1.5个SIMD寄存器,在任何地方都不是最佳的)。

(对于手写的SIMD实现(比你想象的要难),首先要确保对齐数据。接下来,看一下你的指令在目标机器上有什么延迟,并先做最长的例如,在Prescott之前的英特尔上,首先将每个组件混洗到一个寄存器然后与它自身相乘是有意义的,即使它使用3个乘法而不是1个,因为shuffle具有很长的延迟。在后面的模型中,一个shuffle需要一个循环,这将是一个完全的反优化 这再次表明将它留给编译器并不是一个坏主意。)

答案 2 :(得分:4)

执行此操作的SIMD代码(使用SSE3):

movaps xmm0,a
movaps xmm1,b
subps xmm0,xmm1
mulps xmm0,xmm0
haddps xmm0,xmm0
haddps xmm0,xmm0

但是你需要四个值向量(x,y,z,0)才能工作。如果你只有三个值,那么你需要做一些摆弄,以获得所需的格式,这将取消上述任何好处。

一般来说,由于CPU的超标量流水线架构,获得性能的最佳方法是对大量数据执行相同的操作,这样您就可以交错各个步骤并执行一些循环展开到避免管道停滞。基于“在修改后不能直接使用值”原则,上面的代码肯定会停止在最后三条指令上 - 第二条指令必须等待前一条指令的结果完成,这在一个不好的情况下流水线系统。

同时对两个或多个不同点的点进行计算可以消除上述瓶颈 - 在等待一次计算的结果时,您可以开始计算下一个点:

movaps xmm0,a1
                  movaps xmm2,a2
movaps xmm1,b1
                  movaps xmm3,b2
subps xmm0,xmm1
                  subps xmm2,xmm3
mulps xmm0,xmm0
                  mulps xmm2,xmm2
haddps xmm0,xmm0
                  haddps xmm2,xmm2
haddps xmm0,xmm0
                  haddps xmm2,xmm2

答案 3 :(得分:3)

如果您想优化某些内容,请首先查看配置文件代码并检查汇编程序输出。

使用gcc -O3(4.6.1)编译后,我们将使用SIMD获得良好的反汇编输出:

movsd   (%rdi), %xmm0
movsd   8(%rdi), %xmm2
subsd   (%rsi), %xmm0
movsd   16(%rdi), %xmm1
subsd   8(%rsi), %xmm2
subsd   16(%rsi), %xmm1
mulsd   %xmm0, %xmm0
mulsd   %xmm2, %xmm2
mulsd   %xmm1, %xmm1
addsd   %xmm2, %xmm0
addsd   %xmm1, %xmm0

答案 4 :(得分:1)

这种类型的问题经常发生在MD模拟中。通常,通过截止和邻居列表减少计算量,因此减少了计算的数量。然而,平方距离的实际计算是完全完成的(使用编译器优化和固定类型float [3]),如问题中所示。

因此,如果你想减少平方计算量,你应该告诉我们更多关于这个问题的信息。

答案 5 :(得分:0)

也许直接传递6个双打作为参数可以使它更快(因为它可以避免数组取消引用):

inline double distsquare_coord(double xa, double ya, double za, 
                               double xb, double yb, double zb) 
{ 
  double dx = xa-yb; double dy=ya-yb; double dz=za-zb;
  return dx*dx + dy*dy + dz*dz; 
}

或许,如果你附近有很多点,你可以通过线性近似其他近点的距离来计算距离(到同一个固定的其他点)。

答案 6 :(得分:0)

如果您可以重新排列数据以同时处理两对输入向量,则可以使用此代码(仅限SSE2)

// @brief Computes two squared distances between two pairs of 3D vectors
// @param a
//  Pointer to the first pair of 3D vectors.
//  The two vectors must be stored with stride 24, i.e. (a + 3) should point to the first component of the second vector in the pair.
//  Must be aligned by 16 (2 doubles).
// @param b
//  Pointer to the second pairs of 3D vectors.
//  The two vectors must be stored with stride 24, i.e. (a + 3) should point to the first component of the second vector in the pair.
//  Must be aligned by 16 (2 doubles).
// @param c
//  Pointer to the output 2 element array.
//  Must be aligned by 16 (2 doubles).
//  The two distances between a and b vectors will be written to c[0] and c[1] respectively.
void (const double * __restrict__ a, const double * __restrict__ b, double * __restrict c) {
    // diff0 = ( a0.y - b0.y, a0.x - b0.x ) = ( d0.y, d0.x )
    __m128d diff0 = _mm_sub_pd(_mm_load_pd(a), _mm_load_pd(b));
    // diff1 = ( a1.x - b1.x, a0.z - b0.z ) = ( d1.x, d0.z )
    __m128d diff1 = _mm_sub_pd(_mm_load_pd(a + 2), _mm_load_pd(b + 2));
    // diff2 = ( a1.z - b1.z, a1.y - b1.y ) = ( d1.z, d1.y )
    __m128d diff2 = _mm_sub_pd(_mm_load_pd(a + 4), _mm_load_pd(b + 4));

    // prod0 = ( d0.y * d0.y, d0.x * d0.x )
    __m128d prod0 = _mm_mul_pd(diff0, diff0);
    // prod1 = ( d1.x * d1.x, d0.z * d0.z )
    __m128d prod1 = _mm_mul_pd(diff1, diff1);
    // prod2 = ( d1.z * d1.z, d1.y * d1.y )
    __m128d prod2 = _mm_mul_pd(diff1, diff1);

    // _mm_unpacklo_pd(prod0, prod2) = ( d1.y * d1.y, d0.x * d0.x )
    // psum = ( d1.x * d1.x + d1.y * d1.y, d0.x * d0.x + d0.z * d0.z )
    __m128d psum = _mm_add_pd(_mm_unpacklo_pd(prod0, prod2), prod1);

    // _mm_unpackhi_pd(prod0, prod2) = ( d1.z * d1.z, d0.y * d0.y )
    // dotprod = ( d1.x * d1.x + d1.y * d1.y + d1.z * d1.z, d0.x * d0.x + d0.y * d0.y + d0.z * d0.z )
    __m128d dotprod = _mm_add_pd(_mm_unpackhi_pd(prod0, prod2), psum);

    __mm_store_pd(c, dotprod);
}