为什么SSE标量sqrt(x)比rsqrt(x)* x慢?

时间:2009-10-06 23:45:33

标签: performance assembly floating-point x86 sse

我一直在英特尔Core Duo上分析我们的一些核心数学,并且在查看平方根的各种方法时我注意到一些奇怪的事情:使用SSE标量操作,采用倒数平方根更快并将其乘以得到sqrt,而不是使用原生sqrt操作码!

我正在使用类似的循环测试它:

inline float TestSqrtFunction( float in );

void TestFunc()
{
  #define ARRAYSIZE 4096
  #define NUMITERS 16386
  float flIn[ ARRAYSIZE ]; // filled with random numbers ( 0 .. 2^22 )
  float flOut [ ARRAYSIZE ]; // filled with 0 to force fetch into L1 cache

  cyclecounter.Start();
  for ( int i = 0 ; i < NUMITERS ; ++i )
    for ( int j = 0 ; j < ARRAYSIZE ; ++j )
    {
       flOut[j] = TestSqrtFunction( flIn[j] );
       // unrolling this loop makes no difference -- I tested it.
    }
  cyclecounter.Stop();
  printf( "%d loops over %d floats took %.3f milliseconds",
          NUMITERS, ARRAYSIZE, cyclecounter.Milliseconds() );
}

我已经用TestSqrtFunction的几个不同的身体尝试了这个,我有一些真正让我头晕目眩的时间。迄今为止最糟糕的是使用原生sqrt()函数并让“智能”编译器“优化”。在24ns / float时,使用x87 FPU这很糟糕:

inline float TestSqrtFunction( float in )
{  return sqrt(in); }

我尝试的下一件事是使用内部强制编译器使用SSE的标量sqrt操作码:

inline void SSESqrt( float * restrict pOut, float * restrict pIn )
{
   _mm_store_ss( pOut, _mm_sqrt_ss( _mm_load_ss( pIn ) ) );
   // compiles to movss, sqrtss, movss
}

这更好,11.9ns /浮动。我还尝试了Carmack's wacky Newton-Raphson approximation technique,它比硬件运行得更好,为4.3ns / float,尽管错误为1 in 2 10 (这对我来说太多了)。

当我尝试SSE操作倒数平方根时,doozy,然后使用乘法得到平方根(x * 1 /√x=√x)。尽管这需要两次相关操作,但它是迄今为止最快的解决方案,1.24ns / float并精确到2 -14

inline void SSESqrt_Recip_Times_X( float * restrict pOut, float * restrict pIn )
{
   __m128 in = _mm_load_ss( pIn );
   _mm_store_ss( pOut, _mm_mul_ss( in, _mm_rsqrt_ss( in ) ) );
   // compiles to movss, movaps, rsqrtss, mulss, movss
}

我的问题基本上是提供什么为什么SSE的内置到硬件的平方根操作码比其他两个数学运算中的更慢

我确信这确实是操作本身的成本,因为我已经验证了:

  • 所有数据都适合缓存,和 访问是顺序的
  • 函数内联
  • 展开循环没有区别
  • 编译器标志设置为完全优化(并且组件很好,我检查过)

编辑:stephentyrone正确地指出对长数字串的操作应使用向量化SIMD打包操作,如rsqrtps - 但此处的数组数据结构仅用于测试目的:我真正想要测量的是标量性能,用于无法矢量化的代码。)

6 个答案:

答案 0 :(得分:207)

sqrtss给出了正确的舍入结果。 rsqrtss给出了一个近似到倒数,精确到11位。

sqrtss正在生成更准确的结果,因为需要准确性。对于近似值足够的情况,存在rsqrtss,但需要速度。如果您阅读英特尔的文档,您还会发现一个指令序列(倒数平方根逼近,后跟单个Newton-Raphson步骤),它几乎可以提供完全精度(如果我没记错的话,精度约为23位),并且仍然有点快于sqrtss

编辑:如果速度至关重要,并且您实际上是在循环中为多个值调用它,那么您应该使用这些指令的矢量化版本rsqrtps或{{ 1}},每个指令处理四个浮点数。

答案 1 :(得分:7)

分裂也是如此。 MULSS(a,RCPSS(b))比DIVSS(a,b)快。实际上,即使使用Newton-Raphson迭代提高其精度,它仍然更快。

英特尔和AMD都在其优化手册中推荐了这种技术。在不需要符合IEEE-754标准的应用程序中,使用div / sqrt的唯一原因是代码可读性。

答案 2 :(得分:5)

而不是提供答案,实际上可能是不正确的(我也不会检查或争论缓存和其他东西,让我们说它们是相同的)我会试着指出你可以回答的来源你的问题。
  差异可能在于如何计算sqrt和rsqrt。您可以在此处阅读更多内容http://www.intel.com/products/processor/manuals/。我建议从阅读有关您正在使用的处理器函数开始,有一些信息,特别是关于rsqrt(cpu使用内部查找表,具有巨大的近似值,这使得获得结果更加简单)。似乎rsqrt比sqrt快得多,1次额外的mul操作(这不是昂贵的)可能不会改变这里的情况。

编辑:很少有可能值得一提的事实:
  1.一旦我为我的图形库做了一些微优化,我就用rsqrt来计算向量的长度。 (而不是sqrt,我把它的平方和乘以它的rsqrt,这正是你在测试中所做的),并且表现更好。
  2.使用简单查找表计算rsqrt可能更容易,对于rsqrt,当x变为无穷大时,1 / sqrt(x)变为0,因此对于小x,函数值不会改变(很多),而对于sqrt - 它变为无穷大,所以就是那个简单的情况;)。

另外,澄清一下:我不确定我在链接的书中找到了哪些内容,但我很确定我已经读过rsqrt正在使用一些查找表,它应该只用于,当结果不需要精确的时候,虽然 - 我可能也错了,就像前一段时间一样:)。

答案 3 :(得分:3)

Newton-Raphson收敛到f(x)的零,使用增量等于-f/f',其中f'是导数。

对于x=sqrt(y),您可以尝试使用f(x) = 0解析x f(x) = x^2 - y;

然后增量为:dx = -f/f' = 1/2 (x - y/x) = 1/2 (x^2 - y) / x 它的分歧很慢。

您可以尝试其他功能(例如f(x) = 1/y - 1/x^2),但它们会同样复杂。

现在让我们看看1/sqrt(y)。您可以尝试f(x) = x^2 - 1/y,但同样复杂:例如dx = 2xy / (y*x^2 - 1)f(x)的一个非显而易见的替代选择是:f(x) = y - 1/x^2

然后:dx = -f/f' = (y - 1/x^2) / (2/x^3) = 1/2 * x * (1 - y * x^2)

啊!这不是一个微不足道的表达,但你只有它的倍增,没有分歧。 =&GT;更快!

并且:完整更新步骤new_x = x + dx然后显示为:

x *= 3/2 - y/2 * x * x这也很容易。

答案 4 :(得分:1)

几年前已经有许多其他答案。这是共识正确的地方:

  • rsqrt *指令计算倒数平方根的近似值,大约为11-12位。
  • 通过尾数索引的查找表(即ROM)实现。 (实际上,这是一个压缩的查找表,类似于旧的数学表,它使用对低位的调整来节省晶体管。)
  • 之所以可用,是因为它是FPU用于“实数”平方根算法的初始估计。
  • 还有一个近似的倒数指令,rcp。这两个指令都是FPU如何实现平方根和除法的线索。

这是共识出错的地方:

  • SSE时代的FPU不使用Newton-Raphson来计算平方根。在软件中这是一种很棒的方法,但是在硬件中以这种方式实现它是一个错误。

其他人指出,用于计算倒数平方根的N-R算法具有此更新步骤:

x' = 0.5 * x * (3 - n*x*x);

那是很多与数据相关的乘法和一个减法。

接下来是现代FPU实际使用的算法。

给出b[0] = n,假设我们可以找到一系列数字Y[i],使得b[n] = b[0] * Y[0]^2 * Y[1]^2 * ... * Y[n]^2接近1。然后考虑:

x[n] = b[0] * Y[0] * Y[1] * ... * Y[n]
y[n] = Y[0] * Y[1] * ... * Y[n]

显然x[n]接近sqrt(n),而y[n]接近1/sqrt(n)

我们可以使用牛顿-拉夫森(Newton-Raphson)更新步骤来求平方根的倒数,以获得良好的Y[i]

b[i] = b[i-1] * Y[i-1]^2
Y[i] = 0.5 * (3 - b[i])

然后:

x[0] = n Y[0]
x[i] = x[i-1] * Y[i]

和:

y[0] = Y[0]
y[i] = y[i-1] * Y[i]

下一个关键观察结果是b[i] = x[i-1] * y[i-1]。所以:

Y[i] = 0.5 * (3 - x[i-1] * y[i-1])
     = 1 + 0.5 * (1 - x[i-1] * y[i-1])

然后:

x[i] = x[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = x[i-1] + x[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))
y[i] = y[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = y[i-1] + y[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))

也就是说,给定初始x和y,我们可以使用以下更新步骤:

r = 0.5 * (1 - x * y)
x' = x + x * r
y' = y + y * r

或者,甚至更高级的我们都可以设置h = 0.5 * y。这是初始化:

Y = approx_rsqrt(n)
x = Y * n
h = Y * 0.5

这是更新步骤:

r = 0.5 - x * h
x' = x + x * r
h' = h + h * r

这是Goldschmidt的算法,如果在硬件中实现它,则具有巨大的优势:“内部循环”是三个乘法加法,没有别的,其中两个是独立的,可以流水线化。 >

在1999年,FPU已经需要流水线的加/减电路和流水线的乘法电路,否则SSE不会非常“流动”。在1999年,每个电路只需要一个电路就可以以完全流水线的方式实现此内部循环,而不会浪费很多硬件,只是在平方根上。

今天,当然,今天,我们对程序员进行了融合加法运算。同样,内部循环是三个流水线FMA,即使您不计算平方根,它们通常还是有用的。

答案 5 :(得分:-1)

这些指令忽略舍入模式更快,并且不处理浮点异常或欠标准化数。由于这些原因,管道,推测和执行其他fp指令更加容易。