Carmack?STRONG>QUAKE3中用了下面的算法,它第一ơ在公众场合出现的时候,几乎震住了所有的人。据说该法其实q不是Carmack发明的,它真正的作者是Nvidia?STRONG>Gary TarolliQ未l证实)?/P>
// // 计算参数x的^Ҏ的倒数 // float InvSqrt (float x) { float xhalf = 0.5f*x; int i = *(int*)&x; i = 0x5f3759df - (i >> 1); // 计算W一个近似根 x = *(float*)&i; x = x*(1.5f - xhalf*x*x); // 牛顿q代? return x; }
该算法的本质其实是牛顿q代?/STRONG>QNewton-Raphson MethodQ简UNRQ,而NR的基则是泰勒U数QTaylor SeriesQ。NR是一U求方程的近似根的方法。首先要估计一个与方程的根比较靠近的数|然后Ҏ公式推算下一个更加近似的数|不断重复直到可以获得满意的精度。其公式如下Q?
函数Qy=f(x) 其一阶导CؓQy'=f'(x) 则方E:f(x)=0 的第n+1个近似根? x[n+1] = x[n] - f(x[n]) / f'(x[n])NR最关键的地方在于估计第一个近似根。如果该q似根与真根_靠近的话Q那么只需要少数几ơP代,可以得到满意的解?
现在回过头来看看如何利用牛顿法来解决我们的问题。求qx根的倒数Q实际就是求方程1/(x^2)-a=0的解。将该方E按牛顿q代法的公式展开为:
x[n+1]=1/2*x[n]*(3-a*x[n]*x[n])?/2攑ֈ括号里面Q就得到了上面那个函数的倒数W二行?
接着Q我们要设法估计W一个近似根。这也是上面的函数最奇的地斏V它通过某种Ҏ出了一个与真根非常接近的近似根Q因此它只需要用一ơP代过E就获得了较满意的解。它是怎样做到的呢Q所有的奥妙在于这一行:
i = 0x5f3759df - (i >> 1); // 计算W一个近似根
莫名其妙的语句,不是吗?但仔l想一下的话,q是可以理解的。我们知道,IEEE标准下,floatcd的数据在32位系l上是这栯C的Q大体来说就是这P但省略了很多l节Q有兴趣可以GOOGLEQ:
bitsQ?1 30 ... 0 31Q符号位 30-23Q共8位,保存指数QEQ? 22-0Q共23位,保存数QMQ?
所以,32位的点数用十进制实数表C就是:M*2^E。开根然后倒数是QM^(-1/2)*2^(-E/2)。现在就十分清晰了。语句i>>1其工作就是将指数除以2Q实?^(E/2)的部分。而前面用一个常数减dQ目的就是得到M^(1/2)同时反{所有指数的W号?/P>
至于那个0x5f3759dfQ呃Q我只能_的确是一个超U的Magic Number?/P>
那个Magic Number是可以推导出来的Q但我ƈ不打在q里讨论Q因为实在太J琐了。简单来_其原理如下:因ؓIEEE的QҎ中,数M省略了最前面?Q所以实际的数?+M。如果你在大学上数学课没有打瞌睡的话Q那么当你看?1+M)^(-1/2)q样的Ş式时Q应该会马上联想的到它的泰勒U数展开Q而该展开式的W一就是常数。下面给出简单的推导q程Q?/P>
对于实数R>0Q假讑օ在IEEE的Q点表CZQ? 指数为EQ尾CؓMQ则Q? R^(-1/2) = (1+M)^(-1/2) * 2^(-E/2) ?1+M)^(-1/2)按泰勒数展开Q取W一,得: 原式 = (1-M/2) * 2^(-E/2) = 2^(-E/2) - (M/2) * 2^(-E/2) 如果不考虑指数的符L话, (M/2)*2^(E/2)正是(R>>1)Q? 而在IEEE表示中,指数的符号只需单地加上一个偏Ud可, 而式子的前半部分刚好是个常数Q所以原式可以{化ؓQ? 原式 = C - (M/2)*2^(E/2) = C - (R>>1)Q其中C为常? 所以只需要解方程Q? R^(-1/2) = (1+M)^(-1/2) * 2^(-E/2) = C - (R>>1) 求出令到相对误差最的C值就可以?
上面的推DE只是我个h的理解,q未得到证实。而Chris Lomont则在他的论文中详l讨Z最后那个方E的解法Qƈ试在实际的机器上寻找最佳的常数C。有兴趣的朋友可以在文末扑ֈ他的论文的链接?/P>
所以,所谓的Magic NumberQƈ不是从N元宇宙的某个星系׃时空扭曲而掉到地球上的,而是几百q前有的数学理论。只要熟悉NR和泰勒敎ͼ你我同样有能力作出类似的优化?/P>
?A >GameDev.net上有人做q测试,该函数的相对误差Uؓ0.177585%Q速度比C标准库的sqrt提高过20%。如果增加一ơP代过E,相对误差可以降低到e-004的敎ͼ但速度也会降到和sqrt差不多。据说在DOOM3中,Carmack通过查找表进一步优化了该算法,_ֺq乎完美Q而且速度也比原版提高了一截(正在努力弄源码,谁有发我一份)?/P>
值得注意的是Q在Chris Lomont的演中Q理Z最优秀的常敎ͼ_ֺ最高)?STRONG>0x5f37642fQƈ且在实际试中,如果只用一ơP代的话,其效果也是最好的。但奇怪的是,l过两次NR后,在该常数下解的精度将降低得非常厉宻I天知道是怎么回事Q)。经q实际的试Q?STRONG>Chris Lomont认ؓQ最优秀的常数是0x5f375a86。如果换?4位的double版本的话Q算法还是一LQ而最优常数则?STRONG>0x5fe6ec85e7de30daQ又一个o人冒汗的Magic Number - -bQ?/P>
q个法依赖于QҎ的内部表C和字节序Q所以是不具UL性的。如果放到Mac上跑׃挂掉。如果想具备可移植性,q是乖乖用sqrt好了。但法思想是通用的。大家可以尝试推一下相应的qx根算法?/P>
下面l出Carmack在QUAKE3中用的qx根算法。Carmack已经QUAKE3的所有源代码捐给开源了Q所以大家可以放心用,不用担心会收到律师信?/P>
// // Carmack在QUAKE3中用的计算qx根的函数 // float CarmSqrt(float x){ union{ int intPart; float floatPart; } convertor; union{ int intPart; float floatPart; } convertor2; convertor.floatPart = x; convertor2.floatPart = x; convertor.intPart = 0x1FBCF800 + (convertor.intPart >> 1); convertor2.intPart = 0x5f3759df - (convertor2.intPart >> 1); return 0.5f*(convertor.floatPart + (x * convertor2.floatPart)); }
另一个基于同L法的更高速度的sqrt实现如下。其只是单地指数除?Qƈ没有考虑数的方栏V要看懂该代码的话必ȝ道,在IEEE点数的格式中,E是由实际的指数加127得到的。例如,如果实数?.1234*2^10Q在点表示中,EQ第23-30位)的值其实ؓ10+127=137。所以下面的代码中,要处?27偏移Q这是常数0x3f800000的作用。我没实际测试过该函敎ͼ所以对其优劣无从评论,但估计其_ֺ应该会降低很多?/P>
float Faster_Sqrtf(float f) { float result; _asm { mov eax, f sub eax, 0x3f800000 sar eax, 1 add eax, 0x3f800000 mov result, eax } return result; }
除了ZNR的方法外Q其他常见的快速算法还?STRONG>多项式D。下面的函数取自?D游戏~程大师技巧》,它用一个多式来近似替代原来的长度方程Q但我搞不清楚作者用的公式是怎么推导出来的(如果你知道的话请告诉我,谢谢Q?/P>
// // q个函数计算?0,0)?x,y)的距,相对误差?.5% // int FastDistance2D(int x, int y) { x = abs(x); y = abs(y); int mn = MIN(x,y); return(x+y-(mn>>1)-(mn>>2)+(mn>>4)); } // // 该函数计?0,0,0)?x,y,z)的距,相对误差?% // float FastDistance3D(float fx, float fy, float fz) { int temp; int x,y,z; // 保所有的gؓ? x = int(fabs(fx) * 1024); y = int(fabs(fy) * 1024); z = int(fabs(fz) * 1024); // 排序 if (y < x) SWAP(x,y,temp) if (z < y) SWAP(y,z,temp) if (y < x) SWAP(x,y,temp) int dist = (z + 11 * (y >> 5) + (x >> 2) ); return((float)(dist >> 10)); }
q有一U方法称?STRONG>Distance EstimatesQ距评伎ͼQ,如下图所C:
octagon(x,y) = min((1/?) * (|x|+|y|), max(|x|,|y|))求出向量v1和v2的长度,则:
?x^2+y^2) = (|v1|+|v2|)/2 * octagon(x,y)
到目前ؓ止我们都在讨论QҎ的方根算法,接下来轮到整数的Ҏ法。也许有为对整型数据求方ҎM意义Q因Z得到cM99^(1/2)=9的结果。通常情况下确实是q样Q但当我们用定Ҏ的时候(定点C然被应用在很多系l上面,例如d堂的GBA之类的手持设备)Q整数的Ҏ法显得非帔R要。对整数开qx的算法如下。我q不打算在这讨论它(事实是我也没有仔l考究Q因为在短期内都不会用到- -bQ,但你可以在文?STRONG>James Ulery的论文中扑ֈ非常详细的推DE?/P>
// // Z阅读的需要,我在下面的宏定义中添加了换行W? // #define step(shift) if((0x40000000l >> shift) + sqrtVal <= val) { val -= (0x40000000l >> shift) + sqrtVal; sqrtVal = (sqrtVal >> 1) | (0x40000000l >> shift); } else { sqrtVal = sqrtVal >> 1; } // // 计算32位整数的qx? // int32 xxgluSqrtFx(int32 val) { // Note: This fast square root function // only works with an even Q_FACTOR int32 sqrtVal = 0; step(0); step(2); step(4); step(6); step(8); step(10); step(12); step(14); step(16); step(18); step(20); step(22); step(24); step(26); step(28); step(30); if(sqrtVal < val) { ++sqrtVal; } sqrtVal <<= (Q_FACTOR)/2; return(sqrtVal); }
关于sqrt的话题早?003q便已在 GameDev.net上得Cq泛的讨论(可见我实在非常火星了Q当然不排除q有其他在冥王星的人,嘿嘿Q。而尝试探I该话题则完全是Z本h的兴和好奇心(换句话说是无知Q。其实现在随着FPU的提升和对向量运的g支持Q大部分pȝ上都提供了快速的sqrt实现。如果是处理大批量的向量的话Q据说最快的Ҏ是用SIMDQ据说而已Q我压根不懂Q,可同步计?个向量?/P>
q里是当q在GameDev.net?A >讨论Q有的东西包括一些高手的评论和几个版本的sqrt的实数倹{?/P>
有关NR和泰勒数的内容Q请参见MathWorld?/P>
q有两篇论文。一是关于Carmack法的推DE;另一是关于整数Ҏ法的推DE:
以上观点仅代表我个h看法Q由于水qx限,如有错漏Q还望指正。此外,如果你有M看法或意见的话,Ƣ迎?A >论坛留言或者给我发E-MailQ?STRONG>wang_yong_cong@msn.com?/P>