数学建模社区-数学中国

标题: 牛B的 John Carmack密码:0x5f3759df--Quake III的源代码分析 [打印本页]

作者: 5800    时间: 2009-11-26 23:24
标题: 牛B的 John Carmack密码:0x5f3759df--Quake III的源代码分析
有人在Quake III(雷神之锤3)的源代码里面发现这么一段用来求平方根的代码:
2 j. Y, f3 s; N5 F5 E1 w7 w/*================SquareRootFloat================*/
2 V4 q8 L* R7 ~float SquareRootFloat(float number) {
+ {/ \0 c, S7 s) P4 K- Plong i;
3 N! x8 }: f: r3 R3 |) Afloat x, y;$ `2 ~# E* G: J
const float f = 1.5F;
) \, E3 \( u# Y% e9 qx = number * 0.5F;
% F- _  E( w* I* k! {$ ry = number;( Y0 P- X7 n$ V* M2 i2 ?
i = * ( long * ) &y;0 w) z) x" k/ O7 C
i = 0x5f3759df - ( i >> 1 ); //注意这一行
! f' B% J0 y3 N9 b5 m) cy = * ( float * ) &i;
' \" |' T6 }! a+ v0 [2 ky = y * ( f - ( x * y * y ) );$ q8 _4 m+ I: ^/ B; Q& M+ O2 A* i0 m
y = y * ( f - ( x * y * y ) );
9 g% T. x9 k! O) @- preturn number * y;7 C8 ~+ A* Z0 G: _/ ?/ q$ M3 e
}
5 X5 I" e; y: Q7 E0x5f3759df? 这是个什么东西? 学过数值分析就知道,算法里面求平方根一般采用
0 l. t! c" t. m的是无限逼近的方法,比如牛顿迭代法,抱歉当年我数值分析学的太烂,也讲不清楚  h  c7 F1 F  \+ C
。简单来说比如求5的平方根,选一个猜测值比如2,那么我们可以这么算
' H+ t5 A/ @" x5 y( g) V5/2 = 2.5; 2.5+2/2 = 2.25; 5/2.25 = **; 2.25+**/2 = **x ...
& F% I( f4 g/ g  r+ N9 y这样反复迭代下去,结果必定收敛于sqrt(5),没错,一般的求平方根都是这么算的" {3 {/ u" Y+ O( A
。而卡马克的不同之处在于,他选择了一个神秘的猜测值0x5f3759df作为起始,使得
) X2 O4 g) W- x* `整个逼近过程收敛速度暴涨,对于Quake III所要求的精度10的负三次方,只需要一
, Y' b. [; }7 ~9 M. O次迭代就能够得到结果。
; F1 ?+ k  f# H4 [5 m: n/ T$ e好吧,如果这还不算牛b,接着看。' ~5 a  d0 d, f7 v6 a) P( H
普渡大学的数学家Chris Lomont看了以后觉得有趣,决定要研究一下卡马克弄出来的
. {- ^* c/ c" F' |1 ^' b这个猜测值有什么奥秘。Lomont也是个牛人,在精心研究之后从理论上也推导出一个7 Y; z: S. n7 a# r4 h
最佳猜测值,和卡马克的数字非常接近, 0x5f37642f。卡马克真牛,他是外星人吗?
! M7 s; \6 {. ^( O! k& r5 r8 S' F& b1 U6 p" {2 G* m! A# I" ~& r3 z
传奇并没有在这里结束。Lomont计算出结果以后非常满意,于是拿自己计算出的起始
, x) K( n/ ~2 a+ p: u3 I8 B值和卡马克的神秘数字做比赛,看看谁的数字能够更快更精确的求得平方根。结果是$ ~! W! C/ u7 ~7 E6 o$ i
卡马克赢了... 谁也不知道卡马克是怎么找到这个数字的。; a0 r7 w: j/ V6 B! \! @( m
最后Lomont怒了,采用暴力方法一个数字一个数字试过来,终于找到一个比卡马克数
  f8 ]$ X7 s& |( r9 g* o6 g字要好上那么一丁点的数字,虽然实际上这两个数字所产生的结果非常近似,这个暴5 M* c5 `! z3 b1 ~
力得出的数字是0x5f375a86。3 |9 [+ e4 ?6 a* }6 }+ n2 G5 k8 m
Lomont为此写下一篇论文,"Fast Inverse Square Root"。% m( n8 R) m( z. {
John Carmack, ID的无价之宝。! v9 G# L. z! j  H: V; T2 `# T
//===============================================================! u9 t% g! v4 n3 ^* n5 D- N; q
日前在书上看到一段使用多项式逼近计算平方根的代码,至今都没搞明白作者是怎样推算出那个公式的。但在尝试解决问题的过程中,学到了不少东西,于是便有了这篇心得,写出来和大家共享。其中有错漏的地方,还请大家多多指教。
5 m; P0 M0 I- E  l8 j的确,正如许多人所说的那样,现在有有FPU,有3DNow,有SIMD,讨论软件算法好像不合时宜。关于sqrt的话题其实早在2003年便已在 GameDev.net上得到了广泛的讨论(可见我实在非常火星了,当然不排除还有其他尚在冥王星的人,嘿嘿)。而尝试探究该话题则完全是出于本人的兴趣和好奇心(换句话说就是无知)。( M2 o& I( \* B% `; w0 s9 D+ Q
我只是个beginner,所以这种大是大非的问题我也说不清楚(在GameDev.net上也有很多类似的争论)。但无论如何,Carmack在DOOM3中还是使用了软件算法,而多知道一点数学知识对3D编程来说也只有好处没坏处。3D图形编程其实就是数学,数学,还是数学。6 q& F9 e0 V  E3 h7 o4 [
文章原本是用HTML编排的,所以只截取了部分有比较有趣的东西放在这里。原文在我的个人主页上,同时也提供了2篇论文的下载:http://greatsorcerer.go2.icpcn.com/info/fastsqrt.html
9 J# F2 J* D% `% N3 i=========================================================
5 }5 Z6 M' t1 W; s$ E; r在3D图形编程中,经常要求平方根或平方根的倒数,例如:求向量的长度或将向量归一化。C数学函数库中的sqrt具有理想的精度,但对于3D游戏程式来说速度太慢。我们希望能够在保证足够的精度的同时,进一步提高速度。0 [" z' `# L  U& {$ w2 Q
Carmack在QUAKE3中使用了下面的算法,它第一次在公众场合出现的时候,几乎震住了所有的人。据说该算法其实并不是Carmack发明的,它真正的作者是Nvidia的Gary Tarolli(未经证实)。7 j) y. U0 l- u0 \
------------------------------------ q: h5 M6 E3 k5 W' _: {& P
//
% }3 l+ m3 n% d/ ?% b2 z) |# |// 计算参数x的平方根的倒数9 `" M& ^; m4 B, J- L0 a
//% e: h# @1 l: g: s3 a3 S4 u8 M
float InvSqrt (float x)1 m  _% [/ N; }9 r) w' g
{$ k" Z4 R7 r' x
float xhalf = 0.5f*x;
% Q/ M" g0 q9 ?* ~- F/ F  p' Uint i = *(int*)&x;+ ^3 C* m/ q; Y, ~
i = 0x5f3759df - (i >> 1); // 计算第一个近似根
# I" I; O, y) \* N1 G/ c8 xx = *(float*)&i;
% [) \+ v- ?# |, dx = x*(1.5f - xhalf*x*x); // 牛顿迭代法) z2 D* J' f. K3 x  u
return x;
0 ]2 ?# S* x6 {" I3 B}
  I; j/ f3 }* _: S----------------------------------
% b+ ?& \2 x1 w' P1 |1 h5 u1 {- W. _该算法的本质其实就是牛顿迭代法(Newton-Raphson Method,简称NR),而NR的基础则是泰勒级数(Taylor Series)。NR是一种求方程的近似根的方法。首先要估计一个与方程的根比较靠近的数值,然后根据公式推算下一个更加近似的数值,不断重复直到可以获得满意的精度。其公式如下:& j0 g+ F+ B" u0 B5 [3 \
-----------------------------------
3 |! I! L3 Z6 p函数:y=f(x)- o' a0 w, i5 R; x
其一阶导数为:y'=f'(x)' Y* f: F" K. G% v5 ]1 x- Y3 H8 z8 z- }
则方程:f(x)=0 的第n+1个近似根为
- Q$ @- f0 b2 p  D' B! O' Gx[n+1] = x[n] - f(x[n]) / f'(x[n])
' i" A* X1 S, u/ D+ o& J2 S-----------------------------------  [' [  [8 r% X, L
NR最关键的地方在于估计第一个近似根。如果该近似根与真根足够靠近的话,那么只需要少数几次迭代,就可以得到满意的解。
. `; X! u* p) Q7 f* a% |7 `现在回过头来看看如何利用牛顿法来解决我们的问题。求平方根的倒数,实际就是求方程1/(x^2)-a=0的解。将该方程按牛顿迭代法的公式展开为:) ]8 ~3 B/ h0 v) s" t
x[n+1]=1/2*x[n]*(3-a*x[n]*x[n]): i* Y4 f/ c4 x& o, ^7 _: E' x
将1/2放到括号里面,就得到了上面那个函数的倒数第二行。4 ~0 Q, H# j* R: g+ J" `. v
接着,我们要设法估计第一个近似根。这也是上面的函数最神奇的地方。它通过某种方法算出了一个与真根非常接近的近似根,因此它只需要使用一次迭代过程就获得了较满意的解。它是怎样做到的呢?所有的奥妙就在于这一行:5 s1 M$ e% o$ e$ g# l  z- b# a
i = 0x5f3759df - (i >> 1); // 计算第一个近似根7 j, S( j" ]: v  \
超级莫名其妙的语句,不是吗?但仔细想一下的话,还是可以理解的。我们知道,IEEE标准下,float类型的数据在32位系统上是这样表示的(大体来说就是这样,但省略了很多细节,有兴趣可以GOOGLE):" M8 E1 g8 z! ^! E
-------------------------------
9 w! o9 k2 Y: j8 Zbits:31 30 ... 0* ^) T, c' {0 m+ o
31:符号位
5 H" B( B8 f8 O# `; D# o, _7 Y30-23:共8位,保存指数(E)0 x4 \4 \$ k# J6 a
22-0:共23位,保存尾数(M)0 o5 s) t9 y% q& j# T
-------------------------------
5 c+ z! v: u( x( |% R所以,32位的浮点数用十进制实数表示就是:M*2^E。开根然后倒数就是:M^(-1/2)*2^(-E/2)。现在就十分清晰了。语句i> >1其工作就是将指数除以2,实现2^(E/2)的部分。而前面用一个常数减去它,目的就是得到M^(1/2)同时反转所有指数的符号。) x7 E3 C" a% G+ \
至于那个0x5f3759df,呃,我只能说,的确是一个超级的Magic Number。, S( O- C. E& _! ~, ~6 d0 N
那个Magic Number是可以推导出来的,但我并不打算在这里讨论,因为实在太繁琐了。简单来说,其原理如下:因为IEEE的浮点数中,尾数M省略了最前面的1,所以实际的尾数是1+M。如果你在大学上数学课没有打瞌睡的话,那么当你看到(1+M)^(-1/2)这样的形式时,应该会马上联想的到它的泰勒级数展开,而该展开式的第一项就是常数。下面给出简单的推导过程:
* H' @- c' |; [  l-------------------------------
# L, r# H) h" }对于实数R>0,假设其在IEEE的浮点表示中,
" @* x# R$ X9 c指数为E,尾数为M,则:7 p+ o& \" Z- R. u9 I' m6 G/ L# U
R^(-1/2): H  B$ r' D: S* g! u# e& o' W& t
= (1+M)^(-1/2) * 2^(-E/2)/ q6 K5 j% j. V/ z
将(1+M)^(-1/2)按泰勒级数展开,取第一项,得:& ]0 X1 P4 t2 W& g: b5 ^" B5 z
原式
. y0 z8 o6 H2 M= (1-M/2) * 2^(-E/2)# `/ [, v& J$ E$ P7 u
= 2^(-E/2) - (M/2) * 2^(-E/2)* r' M, g, b' M6 w  F* R9 W% ]
如果不考虑指数的符号的话,1 W6 e$ w9 X# B# l
(M/2)*2^(E/2)正是(R>>1),; u) n& \4 F- m/ `5 f
而在IEEE表示中,指数的符号只需简单地加上一个偏移即可,4 d  a2 b  b; W8 d) `
而式子的前半部分刚好是个常数,所以原式可以转化为:
; N3 {2 a8 S% z  e原式 = C - (M/2)*2^(E/2) = C - (R>>1),其中C为常数: {% \: u4 y. k; l9 b+ F
所以只需要解方程:6 a- h3 R  L; S5 D3 E) X
R^(-1/2)$ q2 H$ l9 [2 @; k
= (1+M)^(-1/2) * 2^(-E/2)
. @6 X  P$ K3 ]/ C. \= C - (R>>1)% E( }, N% I0 W) R8 U2 e6 E
求出令到相对误差最小的C值就可以了! U& _' E! R6 g% |' V: N
-------------------------------
5 a1 Z- @3 l- z* T$ L4 i- r上面的推导过程只是我个人的理解,并未得到证实。而Chris Lomont则在他的论文中详细讨论了最后那个方程的解法,并尝试在实际的机器上寻找最佳的常数C。有兴趣的朋友可以在文末找到他的论文的链接。2 u3 R* O2 U* j
所以,所谓的Magic Number,并不是从N元宇宙的某个星系由于时空扭曲而掉到地球上的,而是几百年前就有的数学理论。只要熟悉NR和泰勒级数,你我同样有能力作出类似的优化。3 P) W. p6 t+ X& \' f) ]) y
在GameDev.net上有人做过测试,该函数的相对误差约为0.177585%,速度比C标准库的sqrt提高超过20%。如果增加一次迭代过程,相对误差可以降低到e-004 的级数,但速度也会降到和sqrt差不多。据说在DOOM3中,Carmack通过查找表进一步优化了该算法,精度近乎完美,而且速度也比原版提高了一截(正在努力弄源码,谁有发我一份)。
8 s; {2 k) `9 b1 G7 J+ F值得注意的是,在Chris Lomont的演算中,理论上最优秀的常数(精度最高)是0x5f37642f,并且在实际测试中,如果只使用一次迭代的话,其效果也是最好的。但奇怪的是,经过两次NR后,在该常数下解的精度将降低得非常厉害(天知道是怎么回事!)。经过实际的测试,Chris Lomont认为,最优秀的常数是0x5f375a86。如果换成64位的double版本的话,算法还是一样的,而最优常数则为0x5fe6ec85e7de30da(又一个令人冒汗的Magic Number - -b)。7 U# n6 }; D5 d# S
这个算法依赖于浮点数的内部表示和字节顺序,所以是不具移植性的。如果放到Mac上跑就会挂掉。如果想具备可移植性,还是乖乖用sqrt好了。但算法思想是通用的。大家可以尝试推算一下相应的平方根算法。) J7 g4 ]: Z8 ~( ]# E7 ~4 W- j
下面给出Carmack在QUAKE3中使用的平方根算法。Carmack已经将QUAKE3的所有源代码捐给开源了,所以大家可以放心使用,不用担心会受到律师信。! ]! }1 W$ V2 K; T6 Z
---------------------------------
/ U' y! y/ m3 `: S//5 Q, `: z2 d( _" H, ]! {: o
// Carmack在QUAKE3中使用的计算平方根的函数" E1 \/ c$ y# B' W
//
7 {  O3 T$ }4 w+ ofloat CarmSqrt(float x){- ^- D; E- L# j  Q  P- _
union{: B3 W$ Z0 K1 r6 W( L$ d5 ^4 s
int intPart;1 I+ z# f3 V2 m* a. i* }3 O
float floatPart;" o9 g4 |: |- E7 C
} convertor;; ]1 ?4 ]: Y5 M/ U& d
union{  g9 Q1 ~1 e( F  x3 ]
int intPart;/ f* }9 k5 ?7 l- A
float floatPart;
4 a, i1 H3 l: a! N5 z& X} convertor2;% h+ w9 S: J: e
convertor.floatPart = x;2 r! O! N- c# E8 I- P4 v; L/ Q
convertor2.floatPart = x;8 W' X  O& N% N2 X" ]; R
convertor.intPart = 0x1FBCF800 + (convertor.intPart >> 1);
1 w  K7 C8 `3 Z$ W+ w2 A! Iconvertor2.intPart = 0x5f3759df - (convertor2.intPart >> 1);* F- I6 U: s  P
return 0.5f*(convertor.floatPart + (x * convertor2.floatPart));
6 G- F* ]$ @" K) s% O) u}
作者: olh2008    时间: 2009-11-27 08:22
// 计算参数x的平方根的倒数
1 y9 m6 C& C8 }' R. W4 r//+ a7 H2 y4 T7 B1 g- k8 T0 w
float InvSqrt (float x), T2 b9 K8 D) A& D1 P/ _2 {! ]
{/ D3 y/ ]) I# l
float xhalf = 0.5f*x;
- @& _9 c  i0 W! r/ Q: Rint i = *(int*)&x;5 b3 q" T% O  M: A9 W' E, J
i = 0x5f3759df - (i >> 1); // 计算第一个近似根! x/ G$ w  ?6 W+ |* l
x = *(float*)&i;
' \+ T. o$ ^% G8 ?x = x*(1.5f - xhalf*x*x); // 牛顿迭代法
' T( `& I9 M0 O  Z' l! U6 Xreturn x;
# `* K1 I' C- H5 F9 M+ ?. c}
: O2 D% w, e6 f$ U
这个函数好像并不能实现计算x的平方根的倒数,比如我输入参数为100,它得到的却是-NAN




欢迎光临 数学建模社区-数学中国 (http://www.madio.net/) Powered by Discuz! X2.5