约翰·卡马克(John Carmack)不寻常的快速反平方根(雷神三世)


112

John Carmack在Quake III源代码中具有一个特殊功能,该功能可以计算浮点数的反平方根,比常规速度快4倍(float)(1.0/sqrt(x)),其中包括一个奇怪的0x5f3759df常数。请参见下面的代码。有人可以逐行解释这里到底发生了什么,为什么这样做比常规实现快得多?

float Q_rsqrt( float number )
{
  long i;
  float x2, y;
  const float threehalfs = 1.5F;

  x2 = number * 0.5F;
  y  = number;
  i  = * ( long * ) &y;
  i  = 0x5f3759df - ( i >> 1 );
  y  = * ( float * ) &i;
  y  = y * ( threehalfs - ( x2 * y * y ) );

  #ifndef Q3_VM
  #ifdef __linux__
    assert( !isnan(y) );
  #endif
  #endif
  return y;
}


10
这已经写了无数次。请参阅:google.com/search?
q=0x5f3759df

15
不过谢谢 这个问题比“如何在C#中使正数变为负数”要有趣得多。
MusiGenesis


7
天哪,这只是基于牛顿方法的技巧,不是算法的圣杯,请停止谈论它:P
ldog

Answers:


75

仅供参考。卡马克没有写。Terje Mathisen和Gary Tarolli都为此付出了部分(非常适度)的赞誉,以及其他一些来源的赞扬。

神话常数的来源是一个谜。

引用加里·塔罗利的话:

实际上,它正在以整数形式进行浮点计算-花了很长时间才能弄清楚它的工作方式和原因,我再也记不清细节了。

由专家数学家(Chris Lomont)开发的一个稍好一点的常数,常数试图弄清楚原始算法的工作原理是:

float InvSqrt(float x)
{
    float xhalf = 0.5f * x;
    int i = *(int*)&x;              // get bits for floating value
    i = 0x5f375a86 - (i >> 1);      // gives initial guess y0
    x = *(float*)&i;                // convert bits back to float
    x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
    return x;
}

尽管如此,他的最初尝试还是id的sqrt的数学“高级”版本(几乎达到相同的常数),尽管在数学上更“纯净”,但事实却不如Gary最初开发的版本。他无法解释为什么id是如此出色。


4
“数学上的纯正”应该是什么意思?
塔拉

1
我想像一下,第一个猜测可以从合理的常量派生而来,而不是看似随意的。尽管如果您需要技术说明,也可以查找它。我不是数学家,所以关于数学术语的语义讨论不属于SO。
Rushyo

7
这是正是我封装在恐慌引号这个词,以避免这种胡说八道的理由。我想这是假定读者熟悉口语的英语写作。您会认为常识就足够了。我没有用一个模糊的术语,因为我想“你知道吗,我真的很想被一个不屑一顾地寻找原始来源的人在Google上查询”。
Rushyo 2015年

2
好吧,您实际上还没有回答问题。
BJovke '17

1
对于那些想知道他在哪里找到的人:beyond3d.com/content/articles/8
mr5

52

当然,这些天比使用FPU的sqrt(特别是在360 / PS3上)要慢得多,因为在float和int寄存器之间进行交换会导致加载命中存储,而浮点单元可以做倒数平方扎根于硬件。

它只是说明了优化必须如何随着基础硬件的本质变化而发展。


4
它仍然比std :: sqrt()快很多。
塔拉

2
你有资源吗?我想测试运行时,但没有Xbox 360开发工具包。
DucRP

31

Greg HewgillIllidanS4给出了具有出色数学解释的链接。对于那些不想过多关注细节的人,我将在这里进行总结。

除某些例外情况外,任何数学函数都可以用多项式和表示:

y = f(x)

可以精确地转换为:

y = a0 + a1*x + a2*(x^2) + a3*(x^3) + a4*(x^4) + ...

其中a0,a1,a2,...是常量。问题在于,对于许多函数(例如平方根),对于确切值,该和具有无限数量的成员,并且不以某个x ^ n结尾。但是,如果我们停在某个x ^ n仍然可以得到某种精度的结果。

因此,如果我们有:

y = 1/sqrt(x)

在这种特殊情况下,他们决定丢弃所有高于秒的多项式成员,这可能是由于计算速度所致:

y = a0 + a1*x + [...discarded...]

现在,任务已经结束,可以计算a0和a1,以使y与实际值的差异最小。他们计算出最合适的值为:

a0 = 0x5f375a86
a1 = -0.5

因此,当您将其放入等式中时,您将得到:

y = 0x5f375a86 - 0.5*x

这与您在代码中看到的行相同:

i = 0x5f375a86 - (i >> 1);

编辑:实际上在这里 y = 0x5f375a86 - 0.5*xi = 0x5f375a86 - (i >> 1);将float转换为整数不一样,不仅将整数除以2,而且将指数除以2并导致其他一些伪影,但仍然归结为计算一些系数a0,a1,a2 ...。

在这一点上,他们发现此结果的精度不足以达到目的。因此,他们仅执行了牛顿迭代的一个步骤即可提高结果的准确性:

x = x * (1.5f - xhalf * x * x)

他们可以在一个循环中进行更多的迭代,每次迭代都会提高结果,直到满足所需的精度为止。这正是它在CPU / FPU中的工作方式!但是似乎只有一次迭代就足够了,这对于速度也有好处。CPU / FPU会根据需要进行尽可能多的迭代,以达到存储结果的浮点数的精度,并且它具有适用于所有情况的更通用的算法。


简而言之,他们所做的是:

使用(几乎)与CPU / FPU相同的算法,针对1 / sqrt(x)的特殊情况利用初始条件的改善,而不是一路计算出精确的CPU / FPU会停下来但会更早停止,因此提高计算速度。


2
将指针强制转换为long类型是log_2(float)的近似值。将其投射回去大约2 ^长。这意味着您可以使比率近似线性。
wizzwizz4

22

根据前段时间写的这篇不错的文章 ...

代码的魔力,即使您无法遵循,也以i = 0x5f3759df-(i >> 1)突出。线。简化的牛顿-拉夫森(Newton-Raphson)是一种近似,它从猜测开始,然后通过迭代进行完善。利用32位x86处理器的特性,使用整数强制转换将整数i初始设置为要取其反平方的浮点数的值。然后将i设置为0x5f3759df,负号本身向右移动了一位。右移会丢弃i的最低有效位,实际上将其减半。

这真是一本好书。这只是其中的一小部分。


19

我很好奇,看到常量是个浮点数,所以我只写了这段代码,然后用谷歌搜索弹出的整数。

    long i = 0x5F3759DF;
    float* fp = (float*)&i;
    printf("(2^127)^(1/2) = %f\n", *fp);
    //Output
    //(2^127)^(1/2) = 13211836172961054720.000000

看起来常数是“以浮点表示形式的十六进制形式0x5f3759df更好地知道2 ^ 127的平方根的整数近似值” https://mrob.com/pub/math/numbers-18.html

在同一站点上,它解释了整个过程。https://mrob.com/pub/math/numbers-16.html#le009_16


6
这值得更多关注。在意识到这只是2 ^ 127的平方根之后,这一切都变得有意义了
u8y7541
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.