下列代码是在《雷神之锤III竞技场》源代码中的一个函数(已经剥离了C语言预处理器的指令)。其实,最早在2002年(或2003年)时,这段平方根倒数速算法的代码就已经出现在Usenet与其他论坛上了,并且也在程序员圈子里引起了热烈的讨论。
我先把这段代码贴出来,具体如下:
```
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// 2nd iteration, this can be removed
// y = y * ( threehalfs - ( x2 * y * y ) );
return y;
}
```
这段代码初读起来,我是完全不知所云,尤其是那个魔数0x5f3759df,根本不知道它是什么意思,所以,注释里也是 What the fuck。今天的这篇文章里,我主要就是想带你来了解一下这个函数中的代码究竟是怎样出来的。
其实,这个函数的作用是求平方根倒数,即$x^{-1/2}$,也就是下面这个算式:
$$\frac{1}{\sqrt{x}}$$
当然,它算的是近似值。只不过这个近似值的精度很高,而且计算成本比传统的浮点数运算平方根的算法低太多。在以前那个计算资源还不充分的年代,在一些3D游戏场景的计算机图形学中,要求取照明和投影的光照与反射效果,就经常需要计算平方根倒数,而且是大量的计算——对一个曲面上很多的点做平方根倒数的计算。也就是需要用到下面的这个算式,其中的x,y,z是3D坐标上的一个点的三个坐标值。
$$\frac{1}{\sqrt{x^{2}+y^{2}+z^{2}}}$$
基本上来说,在一个3D游戏中,我们每秒钟都需要做上百万次平方根倒数运算。而在计算硬件还不成熟的时代,这些计算都需要软件来完成,计算速度非常慢。
我们要知道,在上世纪90年代,多数浮点数操作的速度更是远远滞后于整数操作。所以,这段代码所带来的作用是非常大的。
# 计算机的浮点数表示
为了讲清楚这段代码,我们需要先了解一下计算机的浮点数表示法。在C语言中,计算机的浮点数表示用的是IEEE 754 标准,这个标准的表现形式其实就是把一个32bits分成三段。
- 第一段占1bit,表示符号位。代称为S(sign)。
- 第二段占8bits,表示指数。代称为E(Exponent)。
- 第三段占23bits,表示尾数。代称为M(Mantissa)。
如下图所示:
然后呢,一个小数的计算方式是下面这个算式:
$$(-1)^{S}\ast(1+\frac{M}{2^{23}})\ast 2^{(E-127)}$$
但是,这个算式基本上来说,完全就是让人一头雾水,摸不着门路。对于浮点数的解释基本上就是下面这张漫画里表现的样子。
下面,让我来试着解释一下浮点数的那三段表示什么意思。
我们再用IEEE 754的那个算式来算一下:
$${(-1)}^0*({1+\frac{4781507}{2^{23}}})*2^{(128-127)}$$
$$1*(1+0.5700000524520874)*2$$
其中:
- 120 的二进制是01111000
- 7717519的二进制是11101011100001010001111
返回过来算一下:
$$(-1)^{0}\ast (1+\frac{7717519}{2^{23}})\ast 2^{(120-127)}$$
$$(1+0.919999957084656)*0.0078125$$
那么我们就可以使用一个直线方程来代替,也就是:
$$\log_{2}(1+m)\approx m+\sigma $$
于是,我们的公式就简化成了:
$$m_y+\sigma+e_y\approx-\frac{1}{2}(m_x+\sigma+e_x)$$
因为$m = (\frac{M}{2^{23}})$,$e = (E-127)$,代入公式,得到:
$$\frac{M_y}{2^{23}}+\sigma+E_y-127$$
首先,初始值为X0,然后找到X0所对应的Y0(把X0代入公式得到Y0 = f(X0)),然后在(X0,Y0)这个点上做一个切线,得到与X轴交汇的X1。再用X1做一次上述的迭代,得到X2,就这样一直迭代下去,一直找到,y = 0时,x的值。
牛顿法的通用公式是:
$$x_{n+1}=x_n-\frac{f(x_n)}{f’(x_n)}$$
于是,对于$y= \frac{1}{\sqrt{x}}$来说,对固定的x(常数),我们求y使得$\frac{1}{y^2}-x=0$,$f(y)= \frac{1}{y^2} -x$ , $f’(y)=\frac{-2}{y^3}$ 。 注意:$f’(y)$是$f(y)$关于y的导数。
代入上述的牛顿法的通用公式后得到:
$$y_{n+1}=y_n-\frac{\frac{1}{y_n^2}-x}{\frac{-2}{y_n^3}}$$