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


106

我一直在Intel 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 / float。我还尝试了Carmack的古怪的Newton-Raphson逼近技术,其运行速度甚至比硬件还要好,为4.3ns / float,尽管误差为2比10(对于我而言,这太过分了)。

当我尝试SSE op求倒数平方根,然后使用乘法运算得到平方根时,就产生了混淆。即使需要两次相关操作,它还是迄今为止最快的解决方案,速度为1.24ns /浮点,精确度为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-但此处的数组数据结构仅用于测试目的:我真正要衡量的是标量性能以用于代码中无法向量化。)


13
x / sqrt(x)= sqrt(x)。或者,换一种说法:x ^ 1 * x ^(-1/2)= x ^(1-1/2)= x ^(1/2)= sqrt(x)
Crashworks

6
当然inline float SSESqrt( float restrict fIn ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; }。但这是一个坏主意,因为如果CPU将浮点数写入堆栈然后立即读回它们-会很容易导致加载命中存储停顿-特别是从向量寄存器到浮点寄存器中以获取返回值是个坏消息。此外,SSE内在函数表示的基础机器操作码无论如何都采用地址操作数。
Crashworks 2010年

4
LHS的重要程度取决于给定x86的特定生成代和步进:我的经验是,在i7之前的任何东西上,在寄存器集(例如FPU到SSE到eax)之间移动数据都是非常糟糕的,而xmm0和堆栈之间的往返由于英特尔的存储转发,后退不是。您可以自己安排时间以确保查看。通常,查看潜在LHS的最简单方法是查看发出的程序集,并查看寄存器组之间数据的处理方式。您的编译器可能会做聪明的事情,也可能不会做。至于正火载体,我这里写了我的结果:bit.ly/9W5zoU
Crashworks

2
对于PowerPC,是的:IBM有一个CPU模拟器,可以通过静态分析预测LHS和许多其他管道气泡。一些PPC还具有LHS的硬件计数器,您可以对其进行轮询。对于x86来说更难。好的配置工具比较稀缺(最近几天VTune有点破),而且重新排序的管道也缺乏确定性。您可以尝试通过测量每个周期的指令来凭经验测量它,这可以通过硬件性能计数器精确地完成。可以使用例如PAPI或PerfSuite(bit.ly/an6cMt)读取“已退休的指令”和“总周期”寄存器。
Crashworks 2010年

2
您还可以简单地在函数上编写一些排列,然后对它们进行计时,以查看是否有任何特别受停顿困扰的情况。英特尔并未发布有关其管道工作方式的许多细节(它们完全LHS完全是个肮脏的秘密),所以我学到的很多东西都是通过研究导致其他架构停滞的场景(例如PPC) ),然后构建一个受控实验以查看x86是否也具有该功能。
Crashworks'2

Answers:


216

sqrtss给出正确的舍入结果。 rsqrtss给出倒数的近似值,精确到大约11位。

sqrtss当需要准确性时,可以产生更准确的结果。 rsqrtss存在一个近似值但需要速度的情况。如果您阅读了英特尔的文档,您还将发现一条指令序列(平方根的倒数,后跟一个牛顿-拉夫森步长),几乎可以提供全精度(大约23位精度,如果我没记错的话)。比快sqrtss

编辑:如果速度至关重要,并且您实际上是在循环中调用许多值,则应该使用这些指令的向量化版本,rsqrtpssqrtps,这两个指令每条处理四个浮点数。


3
n / r步为您提供22位精度(将其加倍);23位将完全准确。
贾斯珀·贝克斯

7
@贾斯珀·贝克斯(Jasper Bekkers):不,不是。首先,float具有24位精度。第二,sqrtss正确地舍入,这需要〜舍入之前50个比特,并且可以不使用在单精度的简单N / R次迭代来实现。
斯蒂芬·佳能

1
这绝对是原因。为了扩展此结果:英特尔的Embree项目(software.intel.com/en-us/articles/…)将向量化用于其数学。您可以从该链接下载源代码,并查看它们如何处理其3/4 D矢量。他们的向量归一化使用rsqrt,然后是newton-raphson的迭代,此迭代非常精确,但仍比1 / ssqrt快!
布兰登·佩弗里

7
一个小警告:如果x为零或无穷大,则 x rsqrt(x)会得出NaN。0 * rsqrt(0)= 0 * INF = NaN。INF rsqrt(INF)= INF * 0 = NaN。因此,NVIDIA GPU上的CUDA将近似单精度平方根计算为recip(rsqrt(x)),而硬件同时提供了倒数平方和倒数平方根的快速近似。显然,也可以进行处理这两种特殊情况的显式检查(但在GPU上速度较慢)。
njuffa 2012年

@BrandonPelfrey在哪个文件中找到了Newton Rhapson步骤?
fredoverflow 2013年

7

划分也是如此。MULSS(a,RCPSS(b))比DIVSS(a,b)快得多。实际上,即使使用牛顿-拉夫森(Newton-Raphson)迭代来提高精度时,它仍然更快。

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


1
Broadwell和更高版本的FP划分性能更好,因此像clang这样的编译器选择在最新的CPU上不对标使用互惠+牛顿,因为通常速度并不快。在大多数循环中,div这不是唯一的操作,因此即使有divps或,总的uop吞吐量也常常成为瓶颈divss。请参阅浮点除法与浮点乘法,在我的回答中有一节说明了为什么rcpps吞吐量不再胜出。(或赢得延迟),以及除以吞吐量/延迟的数字。
彼得·科德斯

如果您对精度的要求太低,可以跳过牛顿迭代,那么a * rcpss(b)可以加快速度,但仍然比a/b
彼得·科德斯

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,函数值不会改变(很多),而对于x sqrt-达到无穷大,所以就是这种简单情况;)。

另外,请澄清一下:我不确定我在链接的书中哪里找到了它,但是我很确定我已经读过rsqrt正在使用一些查找表,并且仅当结果出现时才应使用它不一定要很精确,尽管-我也有可能是错的,就像前一段时间:)。


4

牛顿-拉夫逊收敛到零f(x)使用增量等于-f/f' 其中f'是衍生物。

对于x=sqrt(y),你可以尝试解决f(x) = 0x使用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)

啊! 它不是一个琐碎的表达式,但其中只有乘法,没有除法。=>更快!

并且:完整的更新步骤new_x = x + dx如下:

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


2

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

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

这是共识出错的地方:

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

正如其他人指出的那样,用于计算倒数平方根的NR算法具有此更新步骤:

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,即使您不计算平方根,它们通常也是有用的。


1
相关:编译后GCC的sqrt()如何工作?使用哪种根方法?牛顿-拉夫森?与硬件div / sqrt执行单元设计有一些链接。 快速矢量化的rsqrt和SSE / AVX的倒数取决于精度 -软件中的一次牛顿迭代,带有或不带有FMA,可_mm256_rsqrt_ps与Haswell性能分析一起使用。通常,如果您没有其他工作要做,并且会严重限制分频器的吞吐量,那么通常只是一个好主意。硬件sqrt是单个uop,因此可以与其他工作混合使用。
彼得·科德斯

-2

因为这些指令会忽略舍入模式,并且不处理浮点异常或低范数,所以速度更快。由于这些原因,流水线,推测和执行其他乱序的fp指令要容易得多。


显然是错误的。FMA取决于当前的舍入模式,但是在Haswell及更高版本上,每个时钟的吞吐量为2。拥有两个全流水线FMA单元,Haswell一次最多可以飞行10个FMA。正确的答案是rsqrt很多精度降低,这查表后意味着少得多的工作要做(或者根本没有?),以获得一个首发的猜测。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.