QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 11012|回复: 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# |3 [0 p( s% B( N& g+ B* J/*================SquareRootFloat================*/9 \, B1 ?' L: J3 @2 s7 B
float SquareRootFloat(float number) {! B' `2 n8 S( @& V% y
long i;
& v4 Z0 [; Q- r4 m$ Rfloat x, y;6 N. r2 l% I3 W( D1 h1 r
const float f = 1.5F;
( c1 p; @4 w7 M' x2 Ax = number * 0.5F;
; _  g1 \) \7 a6 c  ?y = number;
1 {1 w5 c# F1 p; B% K1 b/ \i = * ( long * ) &y;
9 M% I9 u7 K2 ~" M4 Q0 xi = 0x5f3759df - ( i >> 1 ); //注意这一行1 C( f' ]; w1 W) |: B; d
y = * ( float * ) &i;( V. x& A0 |6 r( s* e& C
y = y * ( f - ( x * y * y ) );
# M" H# L. r' d9 j; l  M2 O/ E3 Wy = y * ( f - ( x * y * y ) );- W0 X4 e" a# i8 z1 t
return number * y;: b) B9 o" z6 O, N! m% H. k) N
}
, Z$ N" A1 E( o' s' n0x5f3759df? 这是个什么东西? 学过数值分析就知道,算法里面求平方根一般采用
' M! l" l7 z5 B' F, B0 f的是无限逼近的方法,比如牛顿迭代法,抱歉当年我数值分析学的太烂,也讲不清楚
5 m7 h6 |4 b+ A- @。简单来说比如求5的平方根,选一个猜测值比如2,那么我们可以这么算
4 g5 l. S0 \2 Z3 X' A% y+ N5/2 = 2.5; 2.5+2/2 = 2.25; 5/2.25 = **; 2.25+**/2 = **x ...
, b2 o7 E9 E! }/ k8 X4 o# F这样反复迭代下去,结果必定收敛于sqrt(5),没错,一般的求平方根都是这么算的
9 E$ b8 Q% t5 _8 ]8 N& h! [。而卡马克的不同之处在于,他选择了一个神秘的猜测值0x5f3759df作为起始,使得
# [8 ~0 e2 `# `' f整个逼近过程收敛速度暴涨,对于Quake III所要求的精度10的负三次方,只需要一- g) |" T& C0 w
次迭代就能够得到结果。
; z, B+ o. s9 a" U好吧,如果这还不算牛b,接着看。
6 H8 [8 R& [) t; F& Y! B- B3 A" T/ P普渡大学的数学家Chris Lomont看了以后觉得有趣,决定要研究一下卡马克弄出来的
" W3 `# a: Q+ w' U' j2 N这个猜测值有什么奥秘。Lomont也是个牛人,在精心研究之后从理论上也推导出一个1 M# ~' Y# j; p' ?7 H& I
最佳猜测值,和卡马克的数字非常接近, 0x5f37642f。卡马克真牛,他是外星人吗?
% C- T9 Y: D/ w
0 X* v! w. n% Q7 R, R/ ?) D+ y传奇并没有在这里结束。Lomont计算出结果以后非常满意,于是拿自己计算出的起始
/ L. o3 B7 f2 m( j; ~! c, Z值和卡马克的神秘数字做比赛,看看谁的数字能够更快更精确的求得平方根。结果是
% ^6 W9 ^5 e9 ]2 b# ]1 H: M" a卡马克赢了... 谁也不知道卡马克是怎么找到这个数字的。5 Y* |% Q8 W& H& @, h; n
最后Lomont怒了,采用暴力方法一个数字一个数字试过来,终于找到一个比卡马克数
" Y7 C9 f) B( r字要好上那么一丁点的数字,虽然实际上这两个数字所产生的结果非常近似,这个暴! F4 O* p" v& n
力得出的数字是0x5f375a86。
. v; D$ p" l; Z1 P) MLomont为此写下一篇论文,"Fast Inverse Square Root"。7 A8 M- i. P, }# q
John Carmack, ID的无价之宝。0 N1 u7 u; J! }  x0 x* T
//===============================================================+ |" m9 O+ \. m; S
日前在书上看到一段使用多项式逼近计算平方根的代码,至今都没搞明白作者是怎样推算出那个公式的。但在尝试解决问题的过程中,学到了不少东西,于是便有了这篇心得,写出来和大家共享。其中有错漏的地方,还请大家多多指教。
' \/ ^- j+ P  X2 g1 s' w的确,正如许多人所说的那样,现在有有FPU,有3DNow,有SIMD,讨论软件算法好像不合时宜。关于sqrt的话题其实早在2003年便已在 GameDev.net上得到了广泛的讨论(可见我实在非常火星了,当然不排除还有其他尚在冥王星的人,嘿嘿)。而尝试探究该话题则完全是出于本人的兴趣和好奇心(换句话说就是无知)。
! Y7 s  g' S2 p( q- e4 g我只是个beginner,所以这种大是大非的问题我也说不清楚(在GameDev.net上也有很多类似的争论)。但无论如何,Carmack在DOOM3中还是使用了软件算法,而多知道一点数学知识对3D编程来说也只有好处没坏处。3D图形编程其实就是数学,数学,还是数学。' U# o9 Z( d& @: C. s
文章原本是用HTML编排的,所以只截取了部分有比较有趣的东西放在这里。原文在我的个人主页上,同时也提供了2篇论文的下载:http://greatsorcerer.go2.icpcn.com/info/fastsqrt.html4 F. d' w% q; c, f3 g' I; }% M
=========================================================! U9 h+ L' q, h/ D* E
在3D图形编程中,经常要求平方根或平方根的倒数,例如:求向量的长度或将向量归一化。C数学函数库中的sqrt具有理想的精度,但对于3D游戏程式来说速度太慢。我们希望能够在保证足够的精度的同时,进一步提高速度。
8 F7 v; A$ l# E9 G7 ZCarmack在QUAKE3中使用了下面的算法,它第一次在公众场合出现的时候,几乎震住了所有的人。据说该算法其实并不是Carmack发明的,它真正的作者是Nvidia的Gary Tarolli(未经证实)。% Y" o, }+ {3 a2 |. k" d
-----------------------------------$ }5 t1 ?" n7 E3 i6 V
//- a6 F4 M6 ?; |0 K7 \8 a: k6 @; F
// 计算参数x的平方根的倒数
4 f2 j' ^$ _  M& Z$ J& D& @//
8 ^7 r% y  @2 v' \( U9 H* yfloat InvSqrt (float x)9 t9 S% P% g; Q& T; \, g! `
{: B& y8 Z( E, t5 z
float xhalf = 0.5f*x;8 |$ E1 k# p& }
int i = *(int*)&x;
: p( ?) T# Q3 J- bi = 0x5f3759df - (i >> 1); // 计算第一个近似根( S/ L2 R& o3 `/ O
x = *(float*)&i;
: E) K$ j4 U6 ]x = x*(1.5f - xhalf*x*x); // 牛顿迭代法
1 [& a  P+ h1 greturn x;
6 f' n! Y1 w) n$ e8 E# Z7 P; s5 F4 H}6 Z5 b6 C9 G1 j
----------------------------------# R* g* d6 V0 F2 O- X2 j3 Q
该算法的本质其实就是牛顿迭代法(Newton-Raphson Method,简称NR),而NR的基础则是泰勒级数(Taylor Series)。NR是一种求方程的近似根的方法。首先要估计一个与方程的根比较靠近的数值,然后根据公式推算下一个更加近似的数值,不断重复直到可以获得满意的精度。其公式如下:5 F; ~& f' W6 _
-----------------------------------) e$ q- u2 b7 G9 @7 N& x! J
函数:y=f(x)
. U0 Z8 z4 v5 t4 `# G其一阶导数为:y'=f'(x)
% Z  W% S, |% d. g则方程:f(x)=0 的第n+1个近似根为8 X' r  y& [* Y
x[n+1] = x[n] - f(x[n]) / f'(x[n])( q* K' z5 Y7 T
-----------------------------------. f! c. ?. T7 A, [+ |5 b
NR最关键的地方在于估计第一个近似根。如果该近似根与真根足够靠近的话,那么只需要少数几次迭代,就可以得到满意的解。
. @8 }3 w; A. C" z  G现在回过头来看看如何利用牛顿法来解决我们的问题。求平方根的倒数,实际就是求方程1/(x^2)-a=0的解。将该方程按牛顿迭代法的公式展开为:8 d+ b9 b3 O  P) ~
x[n+1]=1/2*x[n]*(3-a*x[n]*x[n]). ?( R" n6 Y( v, ~/ J
将1/2放到括号里面,就得到了上面那个函数的倒数第二行。, T. o, d! @7 `+ t: J
接着,我们要设法估计第一个近似根。这也是上面的函数最神奇的地方。它通过某种方法算出了一个与真根非常接近的近似根,因此它只需要使用一次迭代过程就获得了较满意的解。它是怎样做到的呢?所有的奥妙就在于这一行:  e6 d" ^2 K$ {! I7 P
i = 0x5f3759df - (i >> 1); // 计算第一个近似根
5 P5 p+ u' j5 {" v超级莫名其妙的语句,不是吗?但仔细想一下的话,还是可以理解的。我们知道,IEEE标准下,float类型的数据在32位系统上是这样表示的(大体来说就是这样,但省略了很多细节,有兴趣可以GOOGLE):- X- h8 x* t! Q5 r) A
-------------------------------
' V; u: r* {+ g! Q/ ?% _bits:31 30 ... 0, d3 i5 m: g% [5 \( y8 o4 z1 ~
31:符号位
" f8 A% r# H5 R+ H3 C: L30-23:共8位,保存指数(E); S" W1 S* A* f! E% L2 ]* V& \$ ^
22-0:共23位,保存尾数(M)
" D/ |' F3 |% c, a3 F; F-------------------------------! L* Y2 X1 c* E5 w( W3 N
所以,32位的浮点数用十进制实数表示就是:M*2^E。开根然后倒数就是:M^(-1/2)*2^(-E/2)。现在就十分清晰了。语句i> >1其工作就是将指数除以2,实现2^(E/2)的部分。而前面用一个常数减去它,目的就是得到M^(1/2)同时反转所有指数的符号。
5 s, ]3 r/ t" m% g至于那个0x5f3759df,呃,我只能说,的确是一个超级的Magic Number。
, k* h8 D% w: {) ?& @那个Magic Number是可以推导出来的,但我并不打算在这里讨论,因为实在太繁琐了。简单来说,其原理如下:因为IEEE的浮点数中,尾数M省略了最前面的1,所以实际的尾数是1+M。如果你在大学上数学课没有打瞌睡的话,那么当你看到(1+M)^(-1/2)这样的形式时,应该会马上联想的到它的泰勒级数展开,而该展开式的第一项就是常数。下面给出简单的推导过程:
/ F' v( N: s. x$ K- ?" m+ ^0 n-------------------------------; ?% _6 _* V& r  b
对于实数R>0,假设其在IEEE的浮点表示中,0 n1 ?: n# ?5 W5 C; T& _. v
指数为E,尾数为M,则:
' t& i8 z5 r  G0 dR^(-1/2)# _& P- M. c% D! i* d. T6 L
= (1+M)^(-1/2) * 2^(-E/2)
& y) i5 Q3 W3 a3 G将(1+M)^(-1/2)按泰勒级数展开,取第一项,得:, a, M* |. R3 S! E! O- C( q3 K! W
原式: r! b8 p6 w9 O  N3 ?  {% [* K. m
= (1-M/2) * 2^(-E/2)& u$ c" ^5 e& x) b: i2 y
= 2^(-E/2) - (M/2) * 2^(-E/2); n% L2 X: Z3 \! Q
如果不考虑指数的符号的话,- g" @' @( m/ I& i
(M/2)*2^(E/2)正是(R>>1),
3 D' B. ?2 }. F6 Y而在IEEE表示中,指数的符号只需简单地加上一个偏移即可,& x% x2 F: H. R$ l
而式子的前半部分刚好是个常数,所以原式可以转化为:, e8 G6 a6 u* c0 A: O, u' x
原式 = C - (M/2)*2^(E/2) = C - (R>>1),其中C为常数" M: Y$ L' j# @2 j4 D0 ?
所以只需要解方程:8 K' h% s. U# b# P
R^(-1/2)
7 C9 F- ]4 ?: D# ]! P& h= (1+M)^(-1/2) * 2^(-E/2)3 F* P  {+ k$ Z& W4 f0 U/ h
= C - (R>>1)  L/ U# _2 Y$ `4 R
求出令到相对误差最小的C值就可以了
8 b0 v+ d2 ~0 a" Q: l0 f3 t-------------------------------+ k: L* t: d1 f1 C/ s
上面的推导过程只是我个人的理解,并未得到证实。而Chris Lomont则在他的论文中详细讨论了最后那个方程的解法,并尝试在实际的机器上寻找最佳的常数C。有兴趣的朋友可以在文末找到他的论文的链接。( @. d& J5 q' w# }2 U5 G* z
所以,所谓的Magic Number,并不是从N元宇宙的某个星系由于时空扭曲而掉到地球上的,而是几百年前就有的数学理论。只要熟悉NR和泰勒级数,你我同样有能力作出类似的优化。
; ~8 o9 ~) o  \# D在GameDev.net上有人做过测试,该函数的相对误差约为0.177585%,速度比C标准库的sqrt提高超过20%。如果增加一次迭代过程,相对误差可以降低到e-004 的级数,但速度也会降到和sqrt差不多。据说在DOOM3中,Carmack通过查找表进一步优化了该算法,精度近乎完美,而且速度也比原版提高了一截(正在努力弄源码,谁有发我一份)。& }  E7 Y- J; }5 W9 o" F
值得注意的是,在Chris Lomont的演算中,理论上最优秀的常数(精度最高)是0x5f37642f,并且在实际测试中,如果只使用一次迭代的话,其效果也是最好的。但奇怪的是,经过两次NR后,在该常数下解的精度将降低得非常厉害(天知道是怎么回事!)。经过实际的测试,Chris Lomont认为,最优秀的常数是0x5f375a86。如果换成64位的double版本的话,算法还是一样的,而最优常数则为0x5fe6ec85e7de30da(又一个令人冒汗的Magic Number - -b)。
/ j  p0 b  f  G, X+ f+ [+ Q7 q这个算法依赖于浮点数的内部表示和字节顺序,所以是不具移植性的。如果放到Mac上跑就会挂掉。如果想具备可移植性,还是乖乖用sqrt好了。但算法思想是通用的。大家可以尝试推算一下相应的平方根算法。
4 j( w+ J+ m1 W0 Z& T下面给出Carmack在QUAKE3中使用的平方根算法。Carmack已经将QUAKE3的所有源代码捐给开源了,所以大家可以放心使用,不用担心会受到律师信。
+ [& d  m9 B4 V. U. F8 Z" T+ r---------------------------------7 ?2 ?- |4 V0 M; R$ u6 Q
//6 q2 D- i6 y% `" q( ~  a
// Carmack在QUAKE3中使用的计算平方根的函数
2 f3 v7 v; G7 x+ i# v# R* X& p//7 v( F* H" ^( ^6 C* U# R5 @4 {" I
float CarmSqrt(float x){0 k5 U# T2 N$ a: g+ Q) ~5 p
union{, |( n  p& N; k- k) S% f/ Q/ }0 D
int intPart;
$ R# Y& X5 i7 g$ L4 `, J/ W3 cfloat floatPart;& T: B! _4 X7 X2 t! l  B3 `, d% s' v
} convertor;. X4 P/ o3 n( C' P. S( g! r( C
union{7 _- c3 `: R" S3 V. J- o
int intPart;1 n4 x" c5 V" c( ?: M0 v  X- N, f
float floatPart;
& S5 |% K0 |2 |# M. b} convertor2;
: [) r; B; i, k1 d8 B. Z! G6 |  Aconvertor.floatPart = x;
& _: j2 h% Z9 {% [8 Wconvertor2.floatPart = x;
# a2 A1 M. T. M# Q6 ]1 ~% s# A6 Cconvertor.intPart = 0x1FBCF800 + (convertor.intPart >> 1);
3 _0 _) L- P3 ^1 ^: B/ _6 o) Y$ hconvertor2.intPart = 0x5f3759df - (convertor2.intPart >> 1);
# F8 }5 s3 l5 N9 @return 0.5f*(convertor.floatPart + (x * convertor2.floatPart));7 g0 q3 e* m9 y# t3 K) R3 C
}
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 _( ^, j% k' O* p$ l9 p' z
    //
    . j: S$ Z% t+ z+ m5 }" {float InvSqrt (float x)
    4 I2 B1 O7 o- W4 A9 A{  m8 r" R: f9 y4 ]
    float xhalf = 0.5f*x;  f+ n7 \5 `6 @) ?& u. T
    int i = *(int*)&x;* E  w; g/ j( n/ n
    i = 0x5f3759df - (i >> 1); // 计算第一个近似根
    0 {  K5 ~3 U8 r4 D- U1 n# mx = *(float*)&i;; I) Q+ X  ^' E- d
    x = x*(1.5f - xhalf*x*x); // 牛顿迭代法9 r5 t6 g* a" ?: w" z
    return x;# z, L: s9 ^7 j- w4 h
    }

    ; r2 k8 a+ i# [3 B9 E4 v1 g5 ^这个函数好像并不能实现计算x的平方根的倒数,比如我输入参数为100,它得到的却是-NAN
    生命,到最后总能成诗……
    回复

    使用道具 举报

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

    qq
    收缩
    • 电话咨询

    • 04714969085
    fastpost

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

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

    蒙公网安备 15010502000194号

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

    GMT+8, 2026-1-1 11:46 , Processed in 1.179157 second(s), 58 queries .

    回顶部