- 在线时间
- 0 小时
- 最后登录
- 2009-10-22
- 注册时间
- 2009-8-18
- 听众数
- 4
- 收听数
- 0
- 能力
- 0 分
- 体力
- 46 点
- 威望
- 0 点
- 阅读权限
- 20
- 积分
- 16
- 相册
- 0
- 日志
- 0
- 记录
- 0
- 帖子
- 3
- 主题
- 1
- 精华
- 0
- 分享
- 0
- 好友
- 0
升级   11.58% 该用户从未签到
 |
有人在Quake III(雷神之锤3)的源代码里面发现这么一段用来求平方根的代码:' B- ?9 w! W W) H
/*================SquareRootFloat================*/ q, x) p% b X/ u( Z
float SquareRootFloat(float number) {5 c4 L% C+ G* H! g
long i;" h$ f2 O( ?- T, q, A% z6 M2 M
float x, y;
' ~ L. S- H! Q+ Wconst float f = 1.5F;1 }$ T( j' W% V1 f0 Y
x = number * 0.5F;8 |# M$ n' h: T, L5 d& Q5 I! F
y = number;: r. F& D- V5 h" j; f) h0 R
i = * ( long * ) &y;
( `) f" ?3 z: l q4 yi = 0x5f3759df - ( i >> 1 ); //注意这一行: Y% e/ z6 `* i8 }5 }1 l! G
y = * ( float * ) &i;, T% t) ]; L% u1 ~; x- ]
y = y * ( f - ( x * y * y ) );' K0 ~. ^' T' v! p
y = y * ( f - ( x * y * y ) );9 ]1 [0 t" A- |' Y7 |$ `
return number * y;# }. W/ ?0 E/ t- r& s
}) {/ z% \5 }5 ]1 N
0x5f3759df? 这是个什么东西? 学过数值分析就知道,算法里面求平方根一般采用6 H g# Y+ ~/ X" Q6 g
的是无限逼近的方法,比如牛顿迭代法,抱歉当年我数值分析学的太烂,也讲不清楚( @6 i! u" Z6 L0 d$ L$ |
。简单来说比如求5的平方根,选一个猜测值比如2,那么我们可以这么算( J8 R1 v' G1 |* x$ y8 n# P
5/2 = 2.5; 2.5+2/2 = 2.25; 5/2.25 = **; 2.25+**/2 = **x ...
5 g% ~# l3 a# o- b, ]1 d# {+ z这样反复迭代下去,结果必定收敛于sqrt(5),没错,一般的求平方根都是这么算的
; Y* X8 @$ v" a v6 Z8 T' l7 b。而卡马克的不同之处在于,他选择了一个神秘的猜测值0x5f3759df作为起始,使得
" y4 |/ |2 v8 ^! Q X整个逼近过程收敛速度暴涨,对于Quake III所要求的精度10的负三次方,只需要一+ j# F6 x' `* K9 M- x
次迭代就能够得到结果。$ x/ z9 e. w& H* d5 e. @
好吧,如果这还不算牛b,接着看。
. U7 |7 |2 P+ L普渡大学的数学家Chris Lomont看了以后觉得有趣,决定要研究一下卡马克弄出来的' O% a5 E6 Q) }/ A, |5 a( P
这个猜测值有什么奥秘。Lomont也是个牛人,在精心研究之后从理论上也推导出一个
- o/ a! l( T, M, J* f最佳猜测值,和卡马克的数字非常接近, 0x5f37642f。卡马克真牛,他是外星人吗?
- S! ?9 ~# k4 s* P7 X# j/ f; U2 U) r! H# A/ D, ^0 s- X* w9 @
传奇并没有在这里结束。Lomont计算出结果以后非常满意,于是拿自己计算出的起始8 w" Q9 |1 q6 ~ Q8 X ?
值和卡马克的神秘数字做比赛,看看谁的数字能够更快更精确的求得平方根。结果是$ g1 }; f" E/ A, _: e
卡马克赢了... 谁也不知道卡马克是怎么找到这个数字的。- X5 J' p- _6 U2 q
最后Lomont怒了,采用暴力方法一个数字一个数字试过来,终于找到一个比卡马克数, y# {7 X" {9 g
字要好上那么一丁点的数字,虽然实际上这两个数字所产生的结果非常近似,这个暴2 z" j* A% T# |1 p1 n
力得出的数字是0x5f375a86。* O: T5 X$ \& t" |: o. T, A" v
Lomont为此写下一篇论文,"Fast Inverse Square Root"。
F& @/ ~* u1 L" OJohn Carmack, ID的无价之宝。4 U+ y v) O4 V1 T: O
//===============================================================) p; y$ X7 V, D2 a6 k8 I
日前在书上看到一段使用多项式逼近计算平方根的代码,至今都没搞明白作者是怎样推算出那个公式的。但在尝试解决问题的过程中,学到了不少东西,于是便有了这篇心得,写出来和大家共享。其中有错漏的地方,还请大家多多指教。
1 R5 y0 l* ^: g" U2 [' _的确,正如许多人所说的那样,现在有有FPU,有3DNow,有SIMD,讨论软件算法好像不合时宜。关于sqrt的话题其实早在2003年便已在 GameDev.net上得到了广泛的讨论(可见我实在非常火星了,当然不排除还有其他尚在冥王星的人,嘿嘿)。而尝试探究该话题则完全是出于本人的兴趣和好奇心(换句话说就是无知)。
# @5 N( m5 z! I# T2 e6 C我只是个beginner,所以这种大是大非的问题我也说不清楚(在GameDev.net上也有很多类似的争论)。但无论如何,Carmack在DOOM3中还是使用了软件算法,而多知道一点数学知识对3D编程来说也只有好处没坏处。3D图形编程其实就是数学,数学,还是数学。
: [( w% {5 v' K2 e( D( `文章原本是用HTML编排的,所以只截取了部分有比较有趣的东西放在这里。原文在我的个人主页上,同时也提供了2篇论文的下载:http://greatsorcerer.go2.icpcn.com/info/fastsqrt.html/ c) _6 W* [4 E& G; E; D3 K
=========================================================
3 E/ p! s9 _+ H y/ Z( d" l. A- z在3D图形编程中,经常要求平方根或平方根的倒数,例如:求向量的长度或将向量归一化。C数学函数库中的sqrt具有理想的精度,但对于3D游戏程式来说速度太慢。我们希望能够在保证足够的精度的同时,进一步提高速度。- ?5 z% Z) h& V" b
Carmack在QUAKE3中使用了下面的算法,它第一次在公众场合出现的时候,几乎震住了所有的人。据说该算法其实并不是Carmack发明的,它真正的作者是Nvidia的Gary Tarolli(未经证实)。
0 [8 o8 H3 {: D, V0 B-----------------------------------$ w8 r. C; v6 Y2 p j2 }" w$ B
//
8 v7 |3 |# D& n ~// 计算参数x的平方根的倒数5 ~1 h1 M8 Y ~0 y5 j9 U0 h2 }
//
7 j. m' n, w+ k& H# dfloat InvSqrt (float x)8 M0 T9 ?2 a" u9 P o7 V
{
& c. {: a2 J$ [9 ~float xhalf = 0.5f*x;
$ S1 O! e+ i; U1 q( V9 yint i = *(int*)&x;
/ a+ N! f9 h1 T: s0 H7 Di = 0x5f3759df - (i >> 1); // 计算第一个近似根
4 v/ ~' N: E% C: c: e* Z0 ~+ F/ Tx = *(float*)&i;9 E" P3 p+ R; X& ?
x = x*(1.5f - xhalf*x*x); // 牛顿迭代法1 A5 a `1 ?* Q/ W) H% L
return x;
! C) ]. z. g8 X2 u" |}& c/ X& | y9 T; t$ a
----------------------------------: p* [9 q5 c2 J. m& s0 H+ d- H( ^
该算法的本质其实就是牛顿迭代法(Newton-Raphson Method,简称NR),而NR的基础则是泰勒级数(Taylor Series)。NR是一种求方程的近似根的方法。首先要估计一个与方程的根比较靠近的数值,然后根据公式推算下一个更加近似的数值,不断重复直到可以获得满意的精度。其公式如下:; q. }$ l( C4 y3 y) i* A
-----------------------------------
4 j2 w" l0 g7 B) F函数:y=f(x)7 ~$ j' ]6 i. ]% c4 l% _8 @
其一阶导数为:y'=f'(x)2 H O( A& n- }
则方程:f(x)=0 的第n+1个近似根为* D% S, N) x5 T0 c4 f% a' f5 \$ Q
x[n+1] = x[n] - f(x[n]) / f'(x[n])
7 a \2 g1 M; `( T-----------------------------------7 U8 M, z* Z2 I4 \
NR最关键的地方在于估计第一个近似根。如果该近似根与真根足够靠近的话,那么只需要少数几次迭代,就可以得到满意的解。
3 _/ o" n q9 ~4 l; a2 L$ e现在回过头来看看如何利用牛顿法来解决我们的问题。求平方根的倒数,实际就是求方程1/(x^2)-a=0的解。将该方程按牛顿迭代法的公式展开为:5 R# f/ l) g+ o2 ?5 f
x[n+1]=1/2*x[n]*(3-a*x[n]*x[n])
8 f! U& r& m y' X4 X! N将1/2放到括号里面,就得到了上面那个函数的倒数第二行。& k. |$ _6 s% `# x) z; E* W
接着,我们要设法估计第一个近似根。这也是上面的函数最神奇的地方。它通过某种方法算出了一个与真根非常接近的近似根,因此它只需要使用一次迭代过程就获得了较满意的解。它是怎样做到的呢?所有的奥妙就在于这一行:
1 e1 s M' D4 L+ c/ e* V9 ui = 0x5f3759df - (i >> 1); // 计算第一个近似根* q1 W8 T m* D, ] F
超级莫名其妙的语句,不是吗?但仔细想一下的话,还是可以理解的。我们知道,IEEE标准下,float类型的数据在32位系统上是这样表示的(大体来说就是这样,但省略了很多细节,有兴趣可以GOOGLE):9 e/ O) u& S3 Z Q
-------------------------------
( \* Z( u. u6 C( Ybits:31 30 ... 02 v$ F/ D7 u# u
31:符号位
! g6 f# H( Y& f30-23:共8位,保存指数(E)
( ~* \1 `) V: \1 S& m) J22-0:共23位,保存尾数(M)0 z1 D' v4 n" f
-------------------------------. i/ }+ P* W" w0 C3 `
所以,32位的浮点数用十进制实数表示就是:M*2^E。开根然后倒数就是:M^(-1/2)*2^(-E/2)。现在就十分清晰了。语句i> >1其工作就是将指数除以2,实现2^(E/2)的部分。而前面用一个常数减去它,目的就是得到M^(1/2)同时反转所有指数的符号。& y( C4 M: `: X' `0 J: A) I8 n
至于那个0x5f3759df,呃,我只能说,的确是一个超级的Magic Number。) t8 Q8 {" }# P8 \# V \! W
那个Magic Number是可以推导出来的,但我并不打算在这里讨论,因为实在太繁琐了。简单来说,其原理如下:因为IEEE的浮点数中,尾数M省略了最前面的1,所以实际的尾数是1+M。如果你在大学上数学课没有打瞌睡的话,那么当你看到(1+M)^(-1/2)这样的形式时,应该会马上联想的到它的泰勒级数展开,而该展开式的第一项就是常数。下面给出简单的推导过程:" N) }6 C) W( b
-------------------------------
7 h9 J4 k, }, z& f! B对于实数R>0,假设其在IEEE的浮点表示中,; z& ~ ^7 y7 a6 c' V
指数为E,尾数为M,则:
& B1 q( ~- ^/ w: u3 z. _, g a, eR^(-1/2)8 @* t5 V* \$ V$ S6 o p: g5 V
= (1+M)^(-1/2) * 2^(-E/2)0 `0 `. {* R& r; \& [8 ~" ?! ~' Y
将(1+M)^(-1/2)按泰勒级数展开,取第一项,得:
9 D( J# n# X5 C6 W4 c2 ~$ J原式
3 E- b; r& `; m4 o$ Z- B0 G= (1-M/2) * 2^(-E/2)
7 v8 Z, [: g" q% N0 y= 2^(-E/2) - (M/2) * 2^(-E/2)
! V8 G5 @: N4 f0 h3 E1 @! m, T$ c如果不考虑指数的符号的话,2 a, ?" D* c7 j: p' s0 ~' `
(M/2)*2^(E/2)正是(R>>1),, b5 L0 W+ K% y8 `2 g
而在IEEE表示中,指数的符号只需简单地加上一个偏移即可,
3 X) J5 E4 K3 `4 i- u7 l& J0 J而式子的前半部分刚好是个常数,所以原式可以转化为:- O# \# F3 p4 C& z
原式 = C - (M/2)*2^(E/2) = C - (R>>1),其中C为常数
. B2 U. `0 V& m* r" Z ^所以只需要解方程:
m2 R" R9 W4 [) c3 a" S- L0 A' RR^(-1/2)# }" |# M' ~; }4 X3 D! |1 Y$ N. A
= (1+M)^(-1/2) * 2^(-E/2)
+ t. y3 N- R/ M. h5 x+ S1 x= C - (R>>1)* c; Y( h6 f" J
求出令到相对误差最小的C值就可以了% }6 d5 E% Y; ?8 Y% H4 l
-------------------------------
* r, h! R! f; I2 M1 ^上面的推导过程只是我个人的理解,并未得到证实。而Chris Lomont则在他的论文中详细讨论了最后那个方程的解法,并尝试在实际的机器上寻找最佳的常数C。有兴趣的朋友可以在文末找到他的论文的链接。1 a8 z+ L& @ ?0 @) v2 D; ~7 A
所以,所谓的Magic Number,并不是从N元宇宙的某个星系由于时空扭曲而掉到地球上的,而是几百年前就有的数学理论。只要熟悉NR和泰勒级数,你我同样有能力作出类似的优化。
: t' d! C2 i# q0 E在GameDev.net上有人做过测试,该函数的相对误差约为0.177585%,速度比C标准库的sqrt提高超过20%。如果增加一次迭代过程,相对误差可以降低到e-004 的级数,但速度也会降到和sqrt差不多。据说在DOOM3中,Carmack通过查找表进一步优化了该算法,精度近乎完美,而且速度也比原版提高了一截(正在努力弄源码,谁有发我一份)。
; ?" W( E, K) Q1 Q4 w8 L( V g( C值得注意的是,在Chris Lomont的演算中,理论上最优秀的常数(精度最高)是0x5f37642f,并且在实际测试中,如果只使用一次迭代的话,其效果也是最好的。但奇怪的是,经过两次NR后,在该常数下解的精度将降低得非常厉害(天知道是怎么回事!)。经过实际的测试,Chris Lomont认为,最优秀的常数是0x5f375a86。如果换成64位的double版本的话,算法还是一样的,而最优常数则为0x5fe6ec85e7de30da(又一个令人冒汗的Magic Number - -b)。+ Q' [- g; i3 M) W3 S" l5 M* W/ M" m
这个算法依赖于浮点数的内部表示和字节顺序,所以是不具移植性的。如果放到Mac上跑就会挂掉。如果想具备可移植性,还是乖乖用sqrt好了。但算法思想是通用的。大家可以尝试推算一下相应的平方根算法。
9 F9 z0 M3 i, D. S: G0 p* w. {下面给出Carmack在QUAKE3中使用的平方根算法。Carmack已经将QUAKE3的所有源代码捐给开源了,所以大家可以放心使用,不用担心会受到律师信。
- x5 q2 p) `- j" m& h, n! O* ?---------------------------------
4 x! o. ^, D' L# z% {2 h, y// K$ g' U! i3 n* z) U
// Carmack在QUAKE3中使用的计算平方根的函数7 b% b+ z! \8 R4 X& I
//
. E4 p+ y* v2 `" [3 {& Z! lfloat CarmSqrt(float x){, Z7 Q' l# }% j- O9 b' S4 m
union{7 ]* x2 I2 q6 \ @1 R+ d
int intPart;
1 c! @9 g {4 |: E J& x$ Xfloat floatPart;" T+ P+ i L6 K% y8 Q, L
} convertor;: F9 R* V3 e' L7 i3 F1 C
union{4 `% F" t; n" H4 e. Z
int intPart;9 l8 V# x4 Z4 U" y: o* c
float floatPart;4 z- z) P; L- J* I8 O" o3 V' Z
} convertor2;% ~! {* s; p5 O# @5 H" R
convertor.floatPart = x;" z. U( X+ \6 u6 n' O
convertor2.floatPart = x;
3 c5 y' V- I. w- y6 k3 v+ qconvertor.intPart = 0x1FBCF800 + (convertor.intPart >> 1);4 ^2 |6 v! ]2 Y( i' B
convertor2.intPart = 0x5f3759df - (convertor2.intPart >> 1);0 L7 D7 K5 B( r: M6 o1 b1 D5 u
return 0.5f*(convertor.floatPart + (x * convertor2.floatPart));
+ B; g I6 F2 O1 }1 }} |
zan
|