现在在x86-64上使用Quake快速求平方根算法是否仍值得?

8

具体来说,我所讨论的是这段代码:

float InvSqrt(float x) {
  float xhalf = 0.5f*x;
  int i = *(int*)&x;        // warning: strict-aliasing UB, use memcpy instead
  i = 0x5f375a86- (i >> 1);
  x = *(float*)&i;          // same
  x = x*(1.5f-xhalf*x*x);
  return x;  
}

我不记得这个算法来自哪里,但它显然比原始的Quake III算法更好、更高效或更精确(稍微不同的魔术常数),但自从这个算法创造出来已经超过两个十年了,我想知道它在性能方面是否仍然值得使用,或者现代x86-64 CPU中是否已经实现了该指令。


这是来自Quake的内容:https://en.wikipedia.org/wiki/Fast_inverse_square_root - Sten Petrov
1
通常现代编译器知道当你以自然的方式编写代码时该怎么做,并选择适当的编译器开关。示例 - njuffa
一个像 1.0f / sqrtf(x) 这样的表达式可能需要类似于 GCC 的 -fno-math-errno 这样的标志 - 你可以使用 pragmas 在本地应用此选项。即使是 sqrtss 后面跟着 divss,也可能与其他指令交错执行以隐藏延迟。 - Brett Hale
1个回答

13

起源:

详见John Carmack的非常规快速求倒平方根(Quake III)


现代用途:无用,已被SSE1的rsqrtss取代

使用_mm_rsqrt_psss来同时获取4个浮点数的近似倒数平方根,比一个好编译器使用这个方法要快得多(使用SSE2整数移位/加法指令将FP位模式保留在XMM寄存器中,这可能不是它实际上会如何在类型安全的C或C++代码中进行编译。即严格别名UB; 使用memcpy或C++20的std::bit_cast)。

https://www.felixcloutier.com/x86/rsqrtss记录了汇编指令的标量版本,包括|相对误差| ≤ 1.5 ∗ 2−12的保证。(即大约有一半的尾数位是正确的)一个牛顿-拉弗森迭代可以将其提炼到接近1ulp的正确程度,尽管仍然不是实际sqrt所得到的0.5ulp。详见SSE/AVX精度相关的快速向量化rsqrt和倒数

rsqrtps指令在大多数CPU上执行的速度与mulps/mulss指令相比只慢了一点,例如5个时钟周期的延迟和1/clock的吞吐量。通过牛顿迭代进行细化后,uops数量更多。延迟因微架构而异,在Zen 3中至少为3 uops,但至少从Conroe开始,Intel的延迟为约5c(https://uops.info/)。

Quake InvSqrt算法中的整数移位/魔术数减法同样提供了一个更粗略的初始猜测,而其余部分(将位模式重新解释为float后)是牛顿-拉弗逊迭代。


编译器甚至会在编译sqrt时自动为您使用rsqrtss,具体取决于上下文和调优选项,例如现代clang编译器使用-O3 -ffast-math -march=skylake并编译1.0f/sqrtf(x)表达式(https://godbolt.org/z/fT86bKesb),使用了vrsqrtss指令和3次vmulss加上一个FMA。非倒数平方根通常不值得,但使用rsqrt+细化可以避免除法以及求平方根。


现在,完全精确的平方根和除法本身并不像以前那么慢,至少如果你与乘法/加法/减法相比很少使用它们。例如,如果你可以隐藏延迟,每进行约12次其他操作就进行一次平方根可能会花费相同的代价,仍然是单个微操作,而不是rsqrt+牛顿迭代需要多个微操作)。参见 浮点除法与浮点乘法

但是,平方根和除法互相竞争吞吐量,因此需要除以平方根时会出现不良情况。

所以,如果你有一个糟糕的循环数组,大部分时间只做平方根计算,而不和其他数学操作混合在一起,这是_mm_rsqrt_ps(和Newton迭代)的用例,作为比_mm_sqrt_ps更高吞吐量的近似值。

但是,如果你可以将该步骤与其他内容结合起来以增加计算强度并在保持div/sqrt单位的同时完成更多重叠工作,则通常最好只使用实际的sqrt指令,因为它仍然只是一个前端要发出的微操作,而且后端要跟踪和执行,与需要5个微操作(如果FMA用于倒数平方根,则更多)的Newton迭代相比较(也适用于需要非倒数平方根的情况)。

例如,Skylake处理器具有每3个时钟周期1次的sqrtps xmm吞吐量(128位向量),如果您不进行多于6个数学运算,则其成本与乘加减/FMA操作相同。对于256位YMM向量,吞吐量更低,需要6个时钟周期。如果UOPs对端口0/1是瓶颈,牛顿迭代将需要更多的UOPs,因此直接使用sqrt会取胜。(假定乱序执行可以隐藏延迟,通常在每个循环迭代都是独立的情况下)如果您在循环中使用多项式逼近作为类似于log或exp的一部分,则这种情况很常见。

另请参见《根据精度快速矢量化rsqrt和倒数,使用SSE/AVX》,了解现代OoO执行CPU性能相关信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接