John Carmack在Quake III源代码中有一个特殊功能,它计算浮点的平方根,比常规(float)(1.0/sqrt(x))
快4倍,包括一个奇怪的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;
}
答案 0 :(得分:66)
FYI。卡马克没有写下来。 Terje Mathisen和Gary Tarolli都为此获得了部分(且非常适度)的信用,并为其他来源提供了信贷。
如何导出神话常数是一件神秘的事。
引用Gary Tarolli:
实际上是浮动的 整数点计算 - 它花了 很长一段时间弄清楚如何以及为什么 这项工作,我不记得了 细节了。
稍微好一点的常量,developed by an expert mathematician(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's sqrt(达到几乎相同的常数)证明不如最初由加里开发的那个,尽管在数学上更“纯粹”。他无法解释为什么id是如此优秀的iirc。
答案 1 :(得分:50)
当然,最近发现它比仅使用FPU的sqrt(特别是在360 / PS3上)要慢得多,因为在float和int寄存器之间进行交换会导致load-hit-store,而浮点单元可以在硬件中做倒数平方根。
它只是展示了随着底层硬件性质的变化,优化必须如何发展。
答案 2 :(得分:25)
Greg Hewgill 和 IllidanS4 提供了一个优秀的数学解释链接。 对于那些不想过多介绍细节的人,我会在这里总结一下。
除了一些例外,任何数学函数都可以用多项式和来表示:
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*x
与i = 0x5f375a86 - (i >> 1);
不同,因为将float作为整数移位不仅除以2而且将exponent除以2并导致其他一些工件,但它仍然归结为计算一些系数a0,a1,a2 ......。
此时他们发现这个结果的精确度不足以达到目的。所以他们另外只做了牛顿迭代的一步来提高结果的准确性:
x = x * (1.5f - xhalf * x * x)
他们可以在一个循环中完成更多的迭代,每个迭代都会改进结果,直到满足所需的精度。 这正是它在CPU / FPU中的工作方式!但似乎只有一次迭代就足够了,这也是对速度的祝福。 CPU / FPU根据需要进行尽可能多的迭代,以达到存储结果的浮点数的精度,并且它具有适用于所有情况的更通用的算法。
简而言之,他们所做的是:
使用(几乎)与CPU / FPU相同的算法,利用1 / sqrt(x)特殊情况的初始条件的改进,并且不计算一直到精度CPU / FPU将去但要提前停止,从而提高计算速度。
答案 3 :(得分:21)
根据to this nice article写了一会儿......
代码的魔力,即使你 不能跟随它,作为i =脱颖而出 0x5f3759df - (i>> 1);线。简化, Newton-Raphson是近似值 从猜测开始 用迭代来细化它。以 32位x86的优点 处理器,i,一个整数,是 最初设置为的值 你想要的浮点数 使用的逆矩阵 整数投射。我被设定为 0x5f3759df,减去自己移位了一个 位于右边。正确的转变 丢掉我最不重要的一点, 基本上将它减半。
这是一个非常好的阅读。这只是它的一小部分。
答案 4 :(得分:12)
我很想知道常量是什么,所以我只是编写了这段代码并用google搜索弹出的整数。
long i = 0x5F3759DF;
float* fp = (float*)&i;
printf("(2^127)^(1/2) = %f\n", *fp);
//Output
//(2^127)^(1/2) = 13211836172961054720.000000
看起来常量是“2 ^ 127的平方根的整数近似,其浮点表示的十六进制形式更为人所知,0x5f3759df”https://mrob.com/pub/math/numbers-18.html
在同一个网站上,它解释了整个事情。 https://mrob.com/pub/math/numbers-16.html#le009_16