QQ登录

只需要一步,快速开始

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

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

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

1

主题

4

听众

16

积分

升级  11.58%

该用户从未签到

新人进步奖

跳转到指定楼层
1#
发表于 2009-11-26 23:24 |只看该作者 |倒序浏览
|招呼Ta 关注Ta
有人在Quake III(雷神之锤3)的源代码里面发现这么一段用来求平方根的代码:
4 l; g3 I* `$ w0 F. L& N% D/*================SquareRootFloat================*/2 E; \! j. L5 o  |2 p: F/ w* [
float SquareRootFloat(float number) {
. ]( }9 N# a) k+ U. m4 Flong i;3 V+ B- k+ P5 x" p, H# z
float x, y;
3 e6 J7 y1 C( s" b1 h; lconst float f = 1.5F;+ Q3 @% o+ V2 Y: x
x = number * 0.5F;: i! I7 I# ^! K8 P( k
y = number;
- K2 y4 M2 \# A$ q+ _3 [i = * ( long * ) &y;5 W& Z: h, s- i
i = 0x5f3759df - ( i >> 1 ); //注意这一行
2 v  M0 E$ b# f0 Oy = * ( float * ) &i;
/ i: {! b# z8 Cy = y * ( f - ( x * y * y ) );2 M( Q2 c$ e. h; [# ?
y = y * ( f - ( x * y * y ) );( c: O1 }3 y" ?% i- u3 \6 O
return number * y;0 n( J6 }3 |3 P- w+ u2 P
}# N5 P4 d4 z- d6 j
0x5f3759df? 这是个什么东西? 学过数值分析就知道,算法里面求平方根一般采用. Q- `; f' s) _; \
的是无限逼近的方法,比如牛顿迭代法,抱歉当年我数值分析学的太烂,也讲不清楚
6 C- n/ P2 W% A9 U+ Z8 Y。简单来说比如求5的平方根,选一个猜测值比如2,那么我们可以这么算
3 P  L- H- q3 D) [5/2 = 2.5; 2.5+2/2 = 2.25; 5/2.25 = **; 2.25+**/2 = **x ...
% l8 B% K( i. {: j4 a6 y8 G$ ^8 h9 t/ l这样反复迭代下去,结果必定收敛于sqrt(5),没错,一般的求平方根都是这么算的
' Z+ s1 H! Y( M" U。而卡马克的不同之处在于,他选择了一个神秘的猜测值0x5f3759df作为起始,使得
7 S( X! |" K+ U) P7 X整个逼近过程收敛速度暴涨,对于Quake III所要求的精度10的负三次方,只需要一
% I/ o4 L' A7 {: z次迭代就能够得到结果。; A8 \$ m1 {( f0 [- w# _
好吧,如果这还不算牛b,接着看。7 j& _9 ?3 _3 Z; j0 J1 x/ _( {. s6 ~
普渡大学的数学家Chris Lomont看了以后觉得有趣,决定要研究一下卡马克弄出来的
6 I3 \0 x+ m7 j3 b  T/ p这个猜测值有什么奥秘。Lomont也是个牛人,在精心研究之后从理论上也推导出一个
" [$ v9 B3 M1 G8 x  h& r最佳猜测值,和卡马克的数字非常接近, 0x5f37642f。卡马克真牛,他是外星人吗?, r& y+ r5 c0 L9 l6 w* V

4 i1 }" m# J# e; q5 p传奇并没有在这里结束。Lomont计算出结果以后非常满意,于是拿自己计算出的起始
0 w; l6 r) Z2 Z/ o7 _值和卡马克的神秘数字做比赛,看看谁的数字能够更快更精确的求得平方根。结果是; {3 d/ `- j9 @3 w. k
卡马克赢了... 谁也不知道卡马克是怎么找到这个数字的。
/ e+ m/ ]6 R" A- Q最后Lomont怒了,采用暴力方法一个数字一个数字试过来,终于找到一个比卡马克数
) w( _* S" [+ ~% {字要好上那么一丁点的数字,虽然实际上这两个数字所产生的结果非常近似,这个暴3 Z9 n/ N/ d. O+ a0 e
力得出的数字是0x5f375a86。
7 F- I, G: p- s( F- OLomont为此写下一篇论文,"Fast Inverse Square Root"。1 j1 N3 P5 f/ U# T
John Carmack, ID的无价之宝。
# e% F  Q9 i* j8 `; h: a; B//===============================================================# Y6 M2 @( H/ F
日前在书上看到一段使用多项式逼近计算平方根的代码,至今都没搞明白作者是怎样推算出那个公式的。但在尝试解决问题的过程中,学到了不少东西,于是便有了这篇心得,写出来和大家共享。其中有错漏的地方,还请大家多多指教。1 G/ O2 @+ P- p& {( T- a
的确,正如许多人所说的那样,现在有有FPU,有3DNow,有SIMD,讨论软件算法好像不合时宜。关于sqrt的话题其实早在2003年便已在 GameDev.net上得到了广泛的讨论(可见我实在非常火星了,当然不排除还有其他尚在冥王星的人,嘿嘿)。而尝试探究该话题则完全是出于本人的兴趣和好奇心(换句话说就是无知)。
$ E; c: R. ?$ S8 j1 g3 I% Z3 l( X我只是个beginner,所以这种大是大非的问题我也说不清楚(在GameDev.net上也有很多类似的争论)。但无论如何,Carmack在DOOM3中还是使用了软件算法,而多知道一点数学知识对3D编程来说也只有好处没坏处。3D图形编程其实就是数学,数学,还是数学。
# [7 ~$ w( N* N" h% f文章原本是用HTML编排的,所以只截取了部分有比较有趣的东西放在这里。原文在我的个人主页上,同时也提供了2篇论文的下载:http://greatsorcerer.go2.icpcn.com/info/fastsqrt.html; w' J; B9 N0 x- l* n; J0 ?
=========================================================3 y# H  a. p1 r
在3D图形编程中,经常要求平方根或平方根的倒数,例如:求向量的长度或将向量归一化。C数学函数库中的sqrt具有理想的精度,但对于3D游戏程式来说速度太慢。我们希望能够在保证足够的精度的同时,进一步提高速度。
- h( k% T$ o1 [+ z2 o$ O0 P( PCarmack在QUAKE3中使用了下面的算法,它第一次在公众场合出现的时候,几乎震住了所有的人。据说该算法其实并不是Carmack发明的,它真正的作者是Nvidia的Gary Tarolli(未经证实)。
  N* D4 `* w% V& Z  ?* d-----------------------------------6 c& ^% P/ m0 V# {) n) j2 F
//4 w* J4 k. h0 M) K0 P* y1 {+ L
// 计算参数x的平方根的倒数
. q* E) Z7 Q3 ]% o2 Y; @# W//
3 O' W/ @6 O! J4 g. ffloat InvSqrt (float x)! y* t0 B; b8 z/ O
{
+ G8 g) d& L% x2 \- c2 L- u$ Q- p* ufloat xhalf = 0.5f*x;
8 w8 y) \8 ~3 ]8 t9 cint i = *(int*)&x;* B% ^8 J4 ^. B% d0 Y
i = 0x5f3759df - (i >> 1); // 计算第一个近似根
8 ~, w9 Y6 Q4 m9 |2 L& Q) P& q% S. \8 Rx = *(float*)&i;
6 N1 B; A. e8 c5 Yx = x*(1.5f - xhalf*x*x); // 牛顿迭代法
$ g. Z0 [& ]" x# i) Wreturn x;
" k9 K% p  J# y  e9 w4 ?0 h}! B# n$ o6 ^4 y! ^
----------------------------------3 t- j; _1 j2 A  u" v
该算法的本质其实就是牛顿迭代法(Newton-Raphson Method,简称NR),而NR的基础则是泰勒级数(Taylor Series)。NR是一种求方程的近似根的方法。首先要估计一个与方程的根比较靠近的数值,然后根据公式推算下一个更加近似的数值,不断重复直到可以获得满意的精度。其公式如下:& S1 J  e. U  E; Y
-----------------------------------
* M$ X! a9 S7 }: C" n函数:y=f(x)
' {: {. N3 g8 U0 n其一阶导数为:y'=f'(x): d) M  L& Q4 p) y& `4 M& Y
则方程:f(x)=0 的第n+1个近似根为
2 ^) j4 g# ~" G' Z* w6 t% mx[n+1] = x[n] - f(x[n]) / f'(x[n])
" s- F4 L! t. m5 ]% _8 k  I8 ?9 _-----------------------------------  K2 M3 D2 B8 m. K4 Y9 F, C0 O) q
NR最关键的地方在于估计第一个近似根。如果该近似根与真根足够靠近的话,那么只需要少数几次迭代,就可以得到满意的解。
% a( ^2 `. d0 g+ A. ^现在回过头来看看如何利用牛顿法来解决我们的问题。求平方根的倒数,实际就是求方程1/(x^2)-a=0的解。将该方程按牛顿迭代法的公式展开为:: Y0 G) O2 `6 S/ d+ e$ W
x[n+1]=1/2*x[n]*(3-a*x[n]*x[n]): k* B7 L2 B( `5 ^: q
将1/2放到括号里面,就得到了上面那个函数的倒数第二行。/ [( ~( a, W8 ^$ p" z
接着,我们要设法估计第一个近似根。这也是上面的函数最神奇的地方。它通过某种方法算出了一个与真根非常接近的近似根,因此它只需要使用一次迭代过程就获得了较满意的解。它是怎样做到的呢?所有的奥妙就在于这一行:6 v6 D( S; l, ], u7 A' F0 Q
i = 0x5f3759df - (i >> 1); // 计算第一个近似根! L/ n7 `) q( A+ ?9 q' S
超级莫名其妙的语句,不是吗?但仔细想一下的话,还是可以理解的。我们知道,IEEE标准下,float类型的数据在32位系统上是这样表示的(大体来说就是这样,但省略了很多细节,有兴趣可以GOOGLE):, r' O6 g! M0 i3 E6 b/ u
-------------------------------
' i6 @6 k5 K' E" X( k0 lbits:31 30 ... 01 T, O8 E# {+ v6 k5 H1 y
31:符号位9 L1 _9 J9 k  f) y: b- P
30-23:共8位,保存指数(E)
- v9 u( X4 N; g& Z1 d0 \22-0:共23位,保存尾数(M)
1 v( @5 s0 d8 Q4 F' V5 Y% i-------------------------------
2 `# G* j1 ^  g+ n( x/ `$ T所以,32位的浮点数用十进制实数表示就是:M*2^E。开根然后倒数就是:M^(-1/2)*2^(-E/2)。现在就十分清晰了。语句i> >1其工作就是将指数除以2,实现2^(E/2)的部分。而前面用一个常数减去它,目的就是得到M^(1/2)同时反转所有指数的符号。1 e7 R) \0 ]$ l5 ~1 T( b( B
至于那个0x5f3759df,呃,我只能说,的确是一个超级的Magic Number。0 @, p4 W& B' N$ {, g! ?1 S
那个Magic Number是可以推导出来的,但我并不打算在这里讨论,因为实在太繁琐了。简单来说,其原理如下:因为IEEE的浮点数中,尾数M省略了最前面的1,所以实际的尾数是1+M。如果你在大学上数学课没有打瞌睡的话,那么当你看到(1+M)^(-1/2)这样的形式时,应该会马上联想的到它的泰勒级数展开,而该展开式的第一项就是常数。下面给出简单的推导过程:
: ]( F" l% `& o4 Y5 n-------------------------------1 _" ]4 A& c$ e& U/ z
对于实数R>0,假设其在IEEE的浮点表示中,
. W! M* ]% |& B0 i' n: ]指数为E,尾数为M,则:
% k7 ^1 }; X' s) l1 oR^(-1/2)
4 X* [) K( |/ K8 ^= (1+M)^(-1/2) * 2^(-E/2)$ y4 b- ]" {- E: d1 u' R
将(1+M)^(-1/2)按泰勒级数展开,取第一项,得:
& j1 v& v6 y7 Y  @; ~4 p7 e原式: n' u: Q9 n% D! q: Q6 v, Y5 J
= (1-M/2) * 2^(-E/2)" P+ M) ]* u. e
= 2^(-E/2) - (M/2) * 2^(-E/2)
5 H6 A# d. ?  O4 W" Y1 d# W4 |如果不考虑指数的符号的话,
6 ]( C5 t0 K1 W1 a+ `% T! u(M/2)*2^(E/2)正是(R>>1),/ C2 Z+ m( @* o
而在IEEE表示中,指数的符号只需简单地加上一个偏移即可,
  p0 [' ]& {$ u" Z" Q, f% N; x而式子的前半部分刚好是个常数,所以原式可以转化为:9 c* ]: ?$ Q, c8 X+ |  V
原式 = C - (M/2)*2^(E/2) = C - (R>>1),其中C为常数0 c* J" k7 S4 ~2 h
所以只需要解方程:8 `% E  o% U; u. [, B; ]
R^(-1/2)6 }# I3 V0 W' l# @: B; t
= (1+M)^(-1/2) * 2^(-E/2)0 M, i  z- S- Q! G4 I
= C - (R>>1)( N6 y% S. I2 l' R- S4 q& g8 _$ k
求出令到相对误差最小的C值就可以了
" k- q6 a  x; N% x" d-------------------------------
2 |+ n, T# @3 `7 _6 l2 ^上面的推导过程只是我个人的理解,并未得到证实。而Chris Lomont则在他的论文中详细讨论了最后那个方程的解法,并尝试在实际的机器上寻找最佳的常数C。有兴趣的朋友可以在文末找到他的论文的链接。$ q8 T6 m+ n$ e  z2 H
所以,所谓的Magic Number,并不是从N元宇宙的某个星系由于时空扭曲而掉到地球上的,而是几百年前就有的数学理论。只要熟悉NR和泰勒级数,你我同样有能力作出类似的优化。
1 f% V3 {% C/ I5 E, F在GameDev.net上有人做过测试,该函数的相对误差约为0.177585%,速度比C标准库的sqrt提高超过20%。如果增加一次迭代过程,相对误差可以降低到e-004 的级数,但速度也会降到和sqrt差不多。据说在DOOM3中,Carmack通过查找表进一步优化了该算法,精度近乎完美,而且速度也比原版提高了一截(正在努力弄源码,谁有发我一份)。6 N4 B5 L8 [" _5 ^) t
值得注意的是,在Chris Lomont的演算中,理论上最优秀的常数(精度最高)是0x5f37642f,并且在实际测试中,如果只使用一次迭代的话,其效果也是最好的。但奇怪的是,经过两次NR后,在该常数下解的精度将降低得非常厉害(天知道是怎么回事!)。经过实际的测试,Chris Lomont认为,最优秀的常数是0x5f375a86。如果换成64位的double版本的话,算法还是一样的,而最优常数则为0x5fe6ec85e7de30da(又一个令人冒汗的Magic Number - -b)。
1 u/ \9 ], ]# q% o9 K0 K* s6 d) U这个算法依赖于浮点数的内部表示和字节顺序,所以是不具移植性的。如果放到Mac上跑就会挂掉。如果想具备可移植性,还是乖乖用sqrt好了。但算法思想是通用的。大家可以尝试推算一下相应的平方根算法。
4 ?  g+ C* q7 n' w/ I2 I  T下面给出Carmack在QUAKE3中使用的平方根算法。Carmack已经将QUAKE3的所有源代码捐给开源了,所以大家可以放心使用,不用担心会受到律师信。
  o  q$ X! x$ O/ W. V* _. C2 [0 z---------------------------------
- Y( `; D: R6 X6 z  k7 i: b//4 X" Y( w: X! l: K8 x" Y; g
// Carmack在QUAKE3中使用的计算平方根的函数
4 s9 N6 A: u; x3 M0 o5 j, K//1 I" C9 C- O- f0 j/ D4 k
float CarmSqrt(float x){1 a( A3 L7 L  _* p
union{& [/ e& G/ R5 Q0 n! r4 R6 p+ ]* P
int intPart;& j) e! P" N: ]0 V& Z4 N* [2 L
float floatPart;" D% `4 k1 g+ W# I2 L
} convertor;
& A; I2 T8 X3 A) m0 nunion{( t3 H% u3 u3 C( O
int intPart;6 y6 u# Y9 r! n; d5 b) h
float floatPart;. r1 E# v# E( v3 i. t
} convertor2;. K- A. n: P5 ~$ g& r/ \7 d
convertor.floatPart = x;
3 r$ |* x6 x# t( U' K, {9 ^convertor2.floatPart = x;
* a9 p; d7 Y+ \2 q8 q! g+ Pconvertor.intPart = 0x1FBCF800 + (convertor.intPart >> 1);1 y9 X! S3 j4 ]9 ~  `: z6 w
convertor2.intPart = 0x5f3759df - (convertor2.intPart >> 1);
0 q" u# `4 ^2 o: k. `4 |# Dreturn 0.5f*(convertor.floatPart + (x * convertor2.floatPart));
- ]7 I6 G5 r, z* Z- Z1 X% |+ o}
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的平方根的倒数
    ! R% u& \) b7 I//- S+ D9 h" ]4 P# ?6 |! X
    float InvSqrt (float x)
    & [5 L9 x2 g! g  y{
    4 l* w2 L, b' Y' w4 Sfloat xhalf = 0.5f*x;: J6 a- R* P  l3 c
    int i = *(int*)&x;- Y4 b5 h+ v& v) s2 p
    i = 0x5f3759df - (i >> 1); // 计算第一个近似根6 E8 g, E  s, G! b* N; T$ j
    x = *(float*)&i;
    " R: m1 @& P; j- T6 lx = x*(1.5f - xhalf*x*x); // 牛顿迭代法, U, {8 x4 |% w+ \1 p
    return x;
    7 {! E" ?; }# d: S}
    0 A0 H' A+ T" E- A" I
    这个函数好像并不能实现计算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-11 07:31 , Processed in 0.419398 second(s), 62 queries .

    回顶部