QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 11054|回复: 1
打印 上一主题 下一主题

牛B的 John Carmack密码:0x5f3759df--Quake III的源代码分析

[复制链接]
字体大小: 正常 放大
5800        

1

主题

4

听众

16

积分

升级  11.58%

该用户从未签到

新人进步奖

跳转到指定楼层
1#
发表于 2009-11-26 23:24 |只看该作者 |倒序浏览
|招呼Ta 关注Ta
有人在Quake III(雷神之锤3)的源代码里面发现这么一段用来求平方根的代码:
' T" e' G; S3 [- K4 g" y. T! q7 ^/*================SquareRootFloat================*/& m2 Q, }% B. ^4 e
float SquareRootFloat(float number) {
( o( ^& U+ e0 ^! K/ L' jlong i;
: t: R* b7 K' E. k5 m/ ]0 J* Hfloat x, y;
+ {1 |1 [( W8 n$ y9 i6 gconst float f = 1.5F;# v( R; b, W: O8 c! S6 T, i
x = number * 0.5F;
$ e# R" h4 [9 n% K" r+ Gy = number;
" F" Z$ ^# N- O6 Wi = * ( long * ) &y;$ {* i* q; F4 v0 s& J+ i- U" G
i = 0x5f3759df - ( i >> 1 ); //注意这一行! b6 {3 d+ _" n! @
y = * ( float * ) &i;6 y: T$ N4 l7 u
y = y * ( f - ( x * y * y ) );8 @% ~9 ~; `) O- [$ {! K
y = y * ( f - ( x * y * y ) );: t- V0 W' a/ ~& u) b
return number * y;  |+ `2 j- u' l) g
}% b1 U, U+ n6 w( n9 l4 S, r
0x5f3759df? 这是个什么东西? 学过数值分析就知道,算法里面求平方根一般采用
$ `$ o1 |. S: A7 w. U4 V的是无限逼近的方法,比如牛顿迭代法,抱歉当年我数值分析学的太烂,也讲不清楚* n. |2 w/ \3 D) w9 Y, E2 `
。简单来说比如求5的平方根,选一个猜测值比如2,那么我们可以这么算8 L% B$ |8 b5 [/ Y/ j/ P
5/2 = 2.5; 2.5+2/2 = 2.25; 5/2.25 = **; 2.25+**/2 = **x ...: t# ]0 Q& f9 E9 N9 i3 @2 s- M
这样反复迭代下去,结果必定收敛于sqrt(5),没错,一般的求平方根都是这么算的
6 b. B) {! `) c3 F) W% K7 V。而卡马克的不同之处在于,他选择了一个神秘的猜测值0x5f3759df作为起始,使得- X! |) E/ a9 M: l/ G) r# |
整个逼近过程收敛速度暴涨,对于Quake III所要求的精度10的负三次方,只需要一1 x4 @0 S, I2 U# }- X0 k3 L0 w6 e
次迭代就能够得到结果。! L6 S/ y: A1 P; _7 _3 }, Q
好吧,如果这还不算牛b,接着看。
# X1 L- R: u- B* R& o0 n% l! X7 C普渡大学的数学家Chris Lomont看了以后觉得有趣,决定要研究一下卡马克弄出来的5 c* S: q6 G" D$ X; V8 R
这个猜测值有什么奥秘。Lomont也是个牛人,在精心研究之后从理论上也推导出一个
8 X  E: ^- f5 a, R9 ]" H$ q最佳猜测值,和卡马克的数字非常接近, 0x5f37642f。卡马克真牛,他是外星人吗?9 j1 z( Q; J/ b" x* W
$ U  {6 c, E' `6 w
传奇并没有在这里结束。Lomont计算出结果以后非常满意,于是拿自己计算出的起始
, e/ r# k1 J- v& I值和卡马克的神秘数字做比赛,看看谁的数字能够更快更精确的求得平方根。结果是2 j4 {' N+ }; c0 N) N5 l- L$ @
卡马克赢了... 谁也不知道卡马克是怎么找到这个数字的。
: `; T" }* U( m4 ]1 r最后Lomont怒了,采用暴力方法一个数字一个数字试过来,终于找到一个比卡马克数9 l* p& Q& v8 k4 G5 l; y; _  a8 M
字要好上那么一丁点的数字,虽然实际上这两个数字所产生的结果非常近似,这个暴+ a/ g8 a1 }: w' ^- N; G1 \
力得出的数字是0x5f375a86。
7 U& n( ?( J. G; sLomont为此写下一篇论文,"Fast Inverse Square Root"。
' F( x& ~/ G7 a4 H" Y9 Z- ?) [John Carmack, ID的无价之宝。5 E# K, i; V2 [
//===============================================================
; f$ r% u+ ~0 p1 h* N  ]日前在书上看到一段使用多项式逼近计算平方根的代码,至今都没搞明白作者是怎样推算出那个公式的。但在尝试解决问题的过程中,学到了不少东西,于是便有了这篇心得,写出来和大家共享。其中有错漏的地方,还请大家多多指教。% D5 Z/ Z2 m7 F5 D9 R0 E
的确,正如许多人所说的那样,现在有有FPU,有3DNow,有SIMD,讨论软件算法好像不合时宜。关于sqrt的话题其实早在2003年便已在 GameDev.net上得到了广泛的讨论(可见我实在非常火星了,当然不排除还有其他尚在冥王星的人,嘿嘿)。而尝试探究该话题则完全是出于本人的兴趣和好奇心(换句话说就是无知)。' n6 H$ A. t+ Z: A" b
我只是个beginner,所以这种大是大非的问题我也说不清楚(在GameDev.net上也有很多类似的争论)。但无论如何,Carmack在DOOM3中还是使用了软件算法,而多知道一点数学知识对3D编程来说也只有好处没坏处。3D图形编程其实就是数学,数学,还是数学。
% z- L: G  M6 x  n! ~+ o4 w; B文章原本是用HTML编排的,所以只截取了部分有比较有趣的东西放在这里。原文在我的个人主页上,同时也提供了2篇论文的下载:http://greatsorcerer.go2.icpcn.com/info/fastsqrt.html
  `" D: r/ r: i6 J6 Z- X=========================================================) i" I$ V$ V2 P" q* _2 I
在3D图形编程中,经常要求平方根或平方根的倒数,例如:求向量的长度或将向量归一化。C数学函数库中的sqrt具有理想的精度,但对于3D游戏程式来说速度太慢。我们希望能够在保证足够的精度的同时,进一步提高速度。
) N* W+ q, |, n) T2 N. |Carmack在QUAKE3中使用了下面的算法,它第一次在公众场合出现的时候,几乎震住了所有的人。据说该算法其实并不是Carmack发明的,它真正的作者是Nvidia的Gary Tarolli(未经证实)。
! _! U& s+ G1 E0 f/ }-----------------------------------1 x; I9 x/ O8 H: d4 t3 ?
//
5 i" l6 z/ M/ j3 r// 计算参数x的平方根的倒数
- z# j8 H$ t* w$ z; M- F9 M//3 {0 s7 X; l% V) ^  V1 n
float InvSqrt (float x)) \9 o' ^9 _4 r5 A2 U! z6 I
{1 N/ ]7 r2 S0 q' J9 F
float xhalf = 0.5f*x;
6 J/ N7 \; ]" mint i = *(int*)&x;/ U$ ]# {) c" i# p
i = 0x5f3759df - (i >> 1); // 计算第一个近似根
$ S6 X  c# \1 z5 hx = *(float*)&i;0 x/ a9 ~5 O# R! B
x = x*(1.5f - xhalf*x*x); // 牛顿迭代法9 _0 g3 b' @3 V& \* P/ W7 ]$ I
return x;
: ?# y0 H3 W* c% A}/ o' _$ b" g2 P  ^! y+ @+ \, w9 h
----------------------------------* X2 |9 [; I. o( ^
该算法的本质其实就是牛顿迭代法(Newton-Raphson Method,简称NR),而NR的基础则是泰勒级数(Taylor Series)。NR是一种求方程的近似根的方法。首先要估计一个与方程的根比较靠近的数值,然后根据公式推算下一个更加近似的数值,不断重复直到可以获得满意的精度。其公式如下:
- I' b  X* w1 I- d-----------------------------------
, q; D- H, A, Z函数:y=f(x)
0 P4 v+ Y; _3 ~- ]其一阶导数为:y'=f'(x)
( h5 K% t+ A' f" }则方程:f(x)=0 的第n+1个近似根为
/ `  e! i: E7 L9 jx[n+1] = x[n] - f(x[n]) / f'(x[n])( G; P% j+ k# q2 h
-----------------------------------
5 G+ E  s8 ~. n" b) L% FNR最关键的地方在于估计第一个近似根。如果该近似根与真根足够靠近的话,那么只需要少数几次迭代,就可以得到满意的解。
8 g9 y: X5 z; ~) [/ i现在回过头来看看如何利用牛顿法来解决我们的问题。求平方根的倒数,实际就是求方程1/(x^2)-a=0的解。将该方程按牛顿迭代法的公式展开为:; ^$ }1 b( K9 N& m( t" i
x[n+1]=1/2*x[n]*(3-a*x[n]*x[n])
3 P( W' V$ m2 E; I( L将1/2放到括号里面,就得到了上面那个函数的倒数第二行。
$ p6 o7 a* O) h8 O% Q+ U% u接着,我们要设法估计第一个近似根。这也是上面的函数最神奇的地方。它通过某种方法算出了一个与真根非常接近的近似根,因此它只需要使用一次迭代过程就获得了较满意的解。它是怎样做到的呢?所有的奥妙就在于这一行:
( D% q( E; v7 M6 P0 Di = 0x5f3759df - (i >> 1); // 计算第一个近似根
- q3 O+ ?& \" g$ X3 [& K9 H超级莫名其妙的语句,不是吗?但仔细想一下的话,还是可以理解的。我们知道,IEEE标准下,float类型的数据在32位系统上是这样表示的(大体来说就是这样,但省略了很多细节,有兴趣可以GOOGLE):
, F. N5 ^$ [. K* ~- I-------------------------------
8 e" p$ e, x4 i( mbits:31 30 ... 0: b# u5 e3 b1 v; }* p3 ]. f* o
31:符号位- q, n/ q9 U# b/ I0 Y. Y
30-23:共8位,保存指数(E)
/ ^9 ^& }7 D3 R! r; R. l0 {22-0:共23位,保存尾数(M)3 u: r/ i) c2 `. @" P0 h" z
-------------------------------
( l# w0 N! A5 |! `9 X* L所以,32位的浮点数用十进制实数表示就是:M*2^E。开根然后倒数就是:M^(-1/2)*2^(-E/2)。现在就十分清晰了。语句i> >1其工作就是将指数除以2,实现2^(E/2)的部分。而前面用一个常数减去它,目的就是得到M^(1/2)同时反转所有指数的符号。
% z- a; ?* s  O& `- O% [$ B) S1 u至于那个0x5f3759df,呃,我只能说,的确是一个超级的Magic Number。
/ l4 W3 s. y! }  O# y那个Magic Number是可以推导出来的,但我并不打算在这里讨论,因为实在太繁琐了。简单来说,其原理如下:因为IEEE的浮点数中,尾数M省略了最前面的1,所以实际的尾数是1+M。如果你在大学上数学课没有打瞌睡的话,那么当你看到(1+M)^(-1/2)这样的形式时,应该会马上联想的到它的泰勒级数展开,而该展开式的第一项就是常数。下面给出简单的推导过程:$ H2 t- M7 p, b9 K2 j
-------------------------------
" x0 ?: N+ \7 _; q% A, T; ?3 i对于实数R>0,假设其在IEEE的浮点表示中,8 d7 M" g" f# z0 p
指数为E,尾数为M,则:% H" Y1 D$ J- @% N( }/ u5 Y$ l
R^(-1/2)
5 }, m! [1 `# _5 O= (1+M)^(-1/2) * 2^(-E/2)0 e' Q4 `; z( z
将(1+M)^(-1/2)按泰勒级数展开,取第一项,得:
8 W! O, A. r! ~. o原式
* ~3 c( z1 d. V4 I" i! ]7 x0 E= (1-M/2) * 2^(-E/2)
, ^) T) u" a/ H7 L= 2^(-E/2) - (M/2) * 2^(-E/2)
( M) r' W! \% B如果不考虑指数的符号的话,+ \: ]8 `+ z; d" g8 E' r! R
(M/2)*2^(E/2)正是(R>>1),
9 ]8 b6 W. @( ~/ s+ M而在IEEE表示中,指数的符号只需简单地加上一个偏移即可,
; A4 K$ ~9 ]% Y' D* H而式子的前半部分刚好是个常数,所以原式可以转化为:" }, A/ m  o6 B7 T& c
原式 = C - (M/2)*2^(E/2) = C - (R>>1),其中C为常数
7 r$ f1 j, W! C: R所以只需要解方程:6 y0 j2 }0 l/ M/ _! D, t+ @
R^(-1/2)
  z, R+ o( d# a* z8 l= (1+M)^(-1/2) * 2^(-E/2)
# O0 K: M3 v: j, J) o= C - (R>>1)
! _4 o4 e1 H5 G6 X/ o  M求出令到相对误差最小的C值就可以了
8 ~# r+ K: x$ d- Q: g-------------------------------* c5 H1 A- H& m* L% ?/ O/ ?
上面的推导过程只是我个人的理解,并未得到证实。而Chris Lomont则在他的论文中详细讨论了最后那个方程的解法,并尝试在实际的机器上寻找最佳的常数C。有兴趣的朋友可以在文末找到他的论文的链接。
/ Z, D1 t/ ?7 X; c- w- e5 A所以,所谓的Magic Number,并不是从N元宇宙的某个星系由于时空扭曲而掉到地球上的,而是几百年前就有的数学理论。只要熟悉NR和泰勒级数,你我同样有能力作出类似的优化。# [0 ?  ]2 A7 [2 U- G- l$ ?$ x2 J
在GameDev.net上有人做过测试,该函数的相对误差约为0.177585%,速度比C标准库的sqrt提高超过20%。如果增加一次迭代过程,相对误差可以降低到e-004 的级数,但速度也会降到和sqrt差不多。据说在DOOM3中,Carmack通过查找表进一步优化了该算法,精度近乎完美,而且速度也比原版提高了一截(正在努力弄源码,谁有发我一份)。; H! H% x% g" z2 x9 k
值得注意的是,在Chris Lomont的演算中,理论上最优秀的常数(精度最高)是0x5f37642f,并且在实际测试中,如果只使用一次迭代的话,其效果也是最好的。但奇怪的是,经过两次NR后,在该常数下解的精度将降低得非常厉害(天知道是怎么回事!)。经过实际的测试,Chris Lomont认为,最优秀的常数是0x5f375a86。如果换成64位的double版本的话,算法还是一样的,而最优常数则为0x5fe6ec85e7de30da(又一个令人冒汗的Magic Number - -b)。8 O+ n$ i" G. k& Q9 h
这个算法依赖于浮点数的内部表示和字节顺序,所以是不具移植性的。如果放到Mac上跑就会挂掉。如果想具备可移植性,还是乖乖用sqrt好了。但算法思想是通用的。大家可以尝试推算一下相应的平方根算法。  `# t( K. T8 ~, h; K' V: X
下面给出Carmack在QUAKE3中使用的平方根算法。Carmack已经将QUAKE3的所有源代码捐给开源了,所以大家可以放心使用,不用担心会受到律师信。
) {( F' V* h0 o8 t! ~---------------------------------0 F9 K9 f# r! \- Q5 }: K3 V* E) x
//8 A1 Q# c4 M( Y, ~/ L. H- o" H" `
// Carmack在QUAKE3中使用的计算平方根的函数* o, O$ g+ O7 ^3 x8 G  I) Y. q+ w9 ]* j3 L1 a
//! Q0 L) d8 x! Q
float CarmSqrt(float x){
7 q* L5 w4 [% Z4 r( N& Yunion{5 F" \* X% r/ T4 m
int intPart;* {$ y. A+ ^2 c6 X
float floatPart;
. P: A8 d" F) h6 S& X+ S} convertor;
* W' ?& t( y4 O' P7 Z; P8 Bunion{5 D8 a. d7 P* I4 w0 K# A$ p
int intPart;- s- k- O) s+ U+ z: T
float floatPart;
8 p- q" _* T: ^2 F: w} convertor2;% g, A) D% k7 @+ m
convertor.floatPart = x;& ]1 F/ V0 x/ h6 k; w
convertor2.floatPart = x;
- W) t0 M. K( Q& f* c+ C3 Fconvertor.intPart = 0x1FBCF800 + (convertor.intPart >> 1);
( j' n% c& y1 u+ B& h# v9 N. }convertor2.intPart = 0x5f3759df - (convertor2.intPart >> 1);
# P4 \- e( B7 o) T1 ureturn 0.5f*(convertor.floatPart + (x * convertor2.floatPart));
/ U! x* a) t8 `" m' k: g& x9 U}
zan
转播转播0 分享淘帖0 分享分享0 收藏收藏0 支持支持0 反对反对0 微信微信
olh2008 实名认证       

88

主题

42

听众

1万

积分

船长

  • TA的每日心情
    开心
    2018-9-1 14:36
  • 签到天数: 86 天

    [LV.6]常住居民II

    邮箱绑定达人 优秀斑竹奖 新人进步奖 发帖功臣 最具活力勋章 元老勋章 原创写作奖 风雨历程奖

    群组Latex研学群

    群组数学建模

    群组Mathematica研究小组

    群组LINGO

    群组Matlab讨论组

    // 计算参数x的平方根的倒数
    2 D7 P# r+ }% u* \  t4 Y4 m4 d4 B//
    0 l" P# A4 P' E. K! I  b3 Mfloat InvSqrt (float x)
    7 Y# F2 C9 A+ Q. {. Z, \; z. ^9 c{
    8 i' C: j1 l: J1 b) Vfloat xhalf = 0.5f*x;; \# ?3 p0 _! o: O
    int i = *(int*)&x;
    3 t! h, N# W& l/ i: ]% mi = 0x5f3759df - (i >> 1); // 计算第一个近似根
    2 t+ p1 |9 P2 O% o) N: Px = *(float*)&i;& S2 V9 }1 _2 `6 o0 }$ q
    x = x*(1.5f - xhalf*x*x); // 牛顿迭代法
    2 `/ `: P$ d4 h/ h! ]return x;) Y8 \# b  X& ^
    }
    7 P- q( l4 v% s3 c
    这个函数好像并不能实现计算x的平方根的倒数,比如我输入参数为100,它得到的却是-NAN
    生命,到最后总能成诗……
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 注册地址

    qq
    收缩
    • 电话咨询

    • 04714969085
    fastpost

    关于我们| 联系我们| 诚征英才| 对外合作| 产品服务| QQ

    手机版|Archiver| |繁體中文 手机客户端  

    蒙公网安备 15010502000194号

    Powered by Discuz! X2.5   © 2001-2013 数学建模网-数学中国 ( 蒙ICP备14002410号-3 蒙BBS备-0002号 )     论坛法律顾问:王兆丰

    GMT+8, 2026-4-14 14:00 , Processed in 0.473816 second(s), 62 queries .

    回顶部