QQ登录

只需要一步,快速开始

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

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

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

1

主题

4

听众

16

积分

升级  11.58%

该用户从未签到

新人进步奖

跳转到指定楼层
1#
发表于 2009-11-26 23:24 |只看该作者 |倒序浏览
|招呼Ta 关注Ta
有人在Quake III(雷神之锤3)的源代码里面发现这么一段用来求平方根的代码:
6 ^" q( \& |) q8 g( r/*================SquareRootFloat================*/
( Y3 p' U- F+ }! e0 Y/ F7 R) Nfloat SquareRootFloat(float number) {; N7 R( M5 N0 b3 C1 B4 }' ~
long i;
" ?4 J6 |: K, a* vfloat x, y;5 L* y' }! E4 y; N# @! N
const float f = 1.5F;
7 n& ], H7 C( ~  f; D1 Zx = number * 0.5F;
& C9 @! d8 P/ x  }  j0 X8 gy = number;2 P! B* {9 [# _! u- |  W' B
i = * ( long * ) &y;
1 u% @  ^0 E- n+ j# m+ G5 qi = 0x5f3759df - ( i >> 1 ); //注意这一行) P8 a- T3 A8 N2 Z4 L
y = * ( float * ) &i;) i: m: U/ F0 J1 {+ X
y = y * ( f - ( x * y * y ) );
1 _  N2 H- w3 Wy = y * ( f - ( x * y * y ) );
" }" T4 n, a: E7 i) z/ q' creturn number * y;
; z# O- g% j! f% a}
9 j: W: m4 p2 X' a. ^0x5f3759df? 这是个什么东西? 学过数值分析就知道,算法里面求平方根一般采用' `' u4 _7 W7 |4 t; p+ I
的是无限逼近的方法,比如牛顿迭代法,抱歉当年我数值分析学的太烂,也讲不清楚
! x- X* z8 q% i。简单来说比如求5的平方根,选一个猜测值比如2,那么我们可以这么算% \* h/ ?( i1 E$ ?
5/2 = 2.5; 2.5+2/2 = 2.25; 5/2.25 = **; 2.25+**/2 = **x .... p1 w8 u3 l1 E; k9 ?% \
这样反复迭代下去,结果必定收敛于sqrt(5),没错,一般的求平方根都是这么算的) e* |. |2 l% w9 ~8 W; K
。而卡马克的不同之处在于,他选择了一个神秘的猜测值0x5f3759df作为起始,使得
) ^& H/ \3 o# t& W6 ?' a* v8 G+ U; W整个逼近过程收敛速度暴涨,对于Quake III所要求的精度10的负三次方,只需要一! Y3 m5 v& D& |  E3 w0 e. Z
次迭代就能够得到结果。6 E# V7 L/ [4 A7 \0 @
好吧,如果这还不算牛b,接着看。
8 Z1 w8 y8 L3 t6 ~7 \普渡大学的数学家Chris Lomont看了以后觉得有趣,决定要研究一下卡马克弄出来的% e8 Y" c' B/ U/ e/ O- T
这个猜测值有什么奥秘。Lomont也是个牛人,在精心研究之后从理论上也推导出一个% @+ b9 N, n4 D  i# A: R6 O+ [- X5 {
最佳猜测值,和卡马克的数字非常接近, 0x5f37642f。卡马克真牛,他是外星人吗?
9 h; C  u# `5 i$ d0 X+ p" P# {( g
  r& @' B+ Z$ I1 |5 C传奇并没有在这里结束。Lomont计算出结果以后非常满意,于是拿自己计算出的起始) W; v0 W! T" a2 {/ K. z7 N
值和卡马克的神秘数字做比赛,看看谁的数字能够更快更精确的求得平方根。结果是) e; W( i3 I7 U/ H* y' H
卡马克赢了... 谁也不知道卡马克是怎么找到这个数字的。
0 t! l* [. c$ q- f7 C, w# N$ ~最后Lomont怒了,采用暴力方法一个数字一个数字试过来,终于找到一个比卡马克数
1 _! H& s7 w' k3 l字要好上那么一丁点的数字,虽然实际上这两个数字所产生的结果非常近似,这个暴8 t* Y7 e" B5 u( i; ]+ @( Q
力得出的数字是0x5f375a86。
5 f" c  H7 `8 q: _( KLomont为此写下一篇论文,"Fast Inverse Square Root"。. }6 Z$ P% u/ ^& p6 D$ H1 C
John Carmack, ID的无价之宝。
3 H# b5 P* B; |, z//===============================================================
) q9 Q3 k' B" z5 i- i, [  w0 y日前在书上看到一段使用多项式逼近计算平方根的代码,至今都没搞明白作者是怎样推算出那个公式的。但在尝试解决问题的过程中,学到了不少东西,于是便有了这篇心得,写出来和大家共享。其中有错漏的地方,还请大家多多指教。
" l9 ^9 ~0 X, f  x" J$ ?2 t的确,正如许多人所说的那样,现在有有FPU,有3DNow,有SIMD,讨论软件算法好像不合时宜。关于sqrt的话题其实早在2003年便已在 GameDev.net上得到了广泛的讨论(可见我实在非常火星了,当然不排除还有其他尚在冥王星的人,嘿嘿)。而尝试探究该话题则完全是出于本人的兴趣和好奇心(换句话说就是无知)。
9 o- {$ b' M- s2 ^! z我只是个beginner,所以这种大是大非的问题我也说不清楚(在GameDev.net上也有很多类似的争论)。但无论如何,Carmack在DOOM3中还是使用了软件算法,而多知道一点数学知识对3D编程来说也只有好处没坏处。3D图形编程其实就是数学,数学,还是数学。
* W7 s# f: r: N4 g7 X" u9 D文章原本是用HTML编排的,所以只截取了部分有比较有趣的东西放在这里。原文在我的个人主页上,同时也提供了2篇论文的下载:http://greatsorcerer.go2.icpcn.com/info/fastsqrt.html3 y- s- w4 T% P2 \) q5 ~0 k
=========================================================! Y- ]( z. |, ]9 j: A' m$ v! h0 \5 G
在3D图形编程中,经常要求平方根或平方根的倒数,例如:求向量的长度或将向量归一化。C数学函数库中的sqrt具有理想的精度,但对于3D游戏程式来说速度太慢。我们希望能够在保证足够的精度的同时,进一步提高速度。
3 \' j6 ]4 S  b$ e. k) y* MCarmack在QUAKE3中使用了下面的算法,它第一次在公众场合出现的时候,几乎震住了所有的人。据说该算法其实并不是Carmack发明的,它真正的作者是Nvidia的Gary Tarolli(未经证实)。
# s- x8 ^& A( d  i0 K; l-----------------------------------
4 `, T- `- y6 C1 @; ?; k//
5 X( L3 R3 c; Z+ k" ], w// 计算参数x的平方根的倒数
: N* p. R- y) ]+ F9 z$ `2 r6 x8 |//
' {% ^) [- A5 R' Y/ Z2 qfloat InvSqrt (float x)
$ @; \. L$ X' C' Z# a! D{
) f0 o) Z( S8 d, B" s' a% Efloat xhalf = 0.5f*x;, B6 g* {6 U: m  Z0 P
int i = *(int*)&x;
3 W; l$ l! x5 u  Ji = 0x5f3759df - (i >> 1); // 计算第一个近似根
6 j; ^1 f" W/ i; I, r2 hx = *(float*)&i;6 L2 Z3 n/ f6 d* F8 w3 w( A
x = x*(1.5f - xhalf*x*x); // 牛顿迭代法: b7 N' O/ x2 E/ @+ w* v
return x;
8 Q$ A" _0 a9 E+ y* |3 [5 s) W}" r$ Y' e( N9 U! o4 i5 ]9 G
----------------------------------! i1 Z9 T0 A4 A" a9 S
该算法的本质其实就是牛顿迭代法(Newton-Raphson Method,简称NR),而NR的基础则是泰勒级数(Taylor Series)。NR是一种求方程的近似根的方法。首先要估计一个与方程的根比较靠近的数值,然后根据公式推算下一个更加近似的数值,不断重复直到可以获得满意的精度。其公式如下:7 |) q. ]0 @& P* k
-----------------------------------6 e; L& Y, [+ |0 ]+ m2 W
函数:y=f(x)
7 t1 c$ H6 ~9 C( G+ Z其一阶导数为:y'=f'(x)% H- @7 ]6 A+ q
则方程:f(x)=0 的第n+1个近似根为
8 s* }4 U+ x# ]) Ux[n+1] = x[n] - f(x[n]) / f'(x[n])
$ d) u# m- P4 A$ s9 Y-----------------------------------
! f1 A' y, Q7 g2 `; mNR最关键的地方在于估计第一个近似根。如果该近似根与真根足够靠近的话,那么只需要少数几次迭代,就可以得到满意的解。
7 B5 R* s% {! j4 a现在回过头来看看如何利用牛顿法来解决我们的问题。求平方根的倒数,实际就是求方程1/(x^2)-a=0的解。将该方程按牛顿迭代法的公式展开为:
7 t. D  x" g5 b" \" l7 w+ |8 Dx[n+1]=1/2*x[n]*(3-a*x[n]*x[n])% F8 l6 A+ e4 {3 G% I
将1/2放到括号里面,就得到了上面那个函数的倒数第二行。
9 O; Z& t' @7 q接着,我们要设法估计第一个近似根。这也是上面的函数最神奇的地方。它通过某种方法算出了一个与真根非常接近的近似根,因此它只需要使用一次迭代过程就获得了较满意的解。它是怎样做到的呢?所有的奥妙就在于这一行:# V7 r& t( o6 S2 g2 G/ Q2 E
i = 0x5f3759df - (i >> 1); // 计算第一个近似根1 R( R" O( j" l. v% K! r
超级莫名其妙的语句,不是吗?但仔细想一下的话,还是可以理解的。我们知道,IEEE标准下,float类型的数据在32位系统上是这样表示的(大体来说就是这样,但省略了很多细节,有兴趣可以GOOGLE):# l. ?, b6 Y8 e. G8 ]4 S
-------------------------------- {+ d* R# {3 Z  ?1 u1 [
bits:31 30 ... 08 @3 h- W( V+ V5 e: _
31:符号位
7 s* f4 _9 k- w; k+ L; R0 L30-23:共8位,保存指数(E)
- d& E4 O6 G3 y3 f6 q* k7 n) j22-0:共23位,保存尾数(M)
6 Z/ q& k. ?* l6 H  ^) d-------------------------------& U8 C* c5 H& ^+ q" ~! T
所以,32位的浮点数用十进制实数表示就是:M*2^E。开根然后倒数就是:M^(-1/2)*2^(-E/2)。现在就十分清晰了。语句i> >1其工作就是将指数除以2,实现2^(E/2)的部分。而前面用一个常数减去它,目的就是得到M^(1/2)同时反转所有指数的符号。" B$ b% a9 I. m$ _+ {5 u; n
至于那个0x5f3759df,呃,我只能说,的确是一个超级的Magic Number。
! W: V- d0 S  D; ?: ?9 Z那个Magic Number是可以推导出来的,但我并不打算在这里讨论,因为实在太繁琐了。简单来说,其原理如下:因为IEEE的浮点数中,尾数M省略了最前面的1,所以实际的尾数是1+M。如果你在大学上数学课没有打瞌睡的话,那么当你看到(1+M)^(-1/2)这样的形式时,应该会马上联想的到它的泰勒级数展开,而该展开式的第一项就是常数。下面给出简单的推导过程:
5 }+ Y  J/ X! {( C-------------------------------
9 d7 V$ C( l5 n" L( e, ~对于实数R>0,假设其在IEEE的浮点表示中,( a0 n; D& ~7 d( W6 F) ~. L( `
指数为E,尾数为M,则:
7 Z; o6 _" l7 b( C2 D' UR^(-1/2): h- V; s( K# o, l4 I6 H
= (1+M)^(-1/2) * 2^(-E/2)" x) @5 Z6 V& {- h. o
将(1+M)^(-1/2)按泰勒级数展开,取第一项,得:
" ?) E7 D4 d4 n原式
$ D2 ]/ Z9 x. A9 c5 r= (1-M/2) * 2^(-E/2)
3 P" `* d7 f* {+ r! F# O7 f6 t= 2^(-E/2) - (M/2) * 2^(-E/2)/ W- X7 U: t# e4 v0 A, U
如果不考虑指数的符号的话,+ p8 _8 A, M  Z' h9 q3 N
(M/2)*2^(E/2)正是(R>>1),
  I3 n5 j* c# H而在IEEE表示中,指数的符号只需简单地加上一个偏移即可,9 O( ?. S1 N* @
而式子的前半部分刚好是个常数,所以原式可以转化为:
/ |9 L- x2 a- Z8 `/ s" ^1 ~2 F原式 = C - (M/2)*2^(E/2) = C - (R>>1),其中C为常数
- ^$ e# X2 ?/ d( |0 ]5 i所以只需要解方程:# Z4 G- R. p% A. o' K, S4 }: p. n* i
R^(-1/2)( R9 x3 p) e  m8 x, H
= (1+M)^(-1/2) * 2^(-E/2)
. p* Q6 K2 D, x6 t7 d) a% K. C= C - (R>>1)' h/ L+ p+ s' ^
求出令到相对误差最小的C值就可以了
! A/ B2 q6 [: X" Q' ]2 e. w" @6 p-------------------------------
( B( K5 R) J+ u* [上面的推导过程只是我个人的理解,并未得到证实。而Chris Lomont则在他的论文中详细讨论了最后那个方程的解法,并尝试在实际的机器上寻找最佳的常数C。有兴趣的朋友可以在文末找到他的论文的链接。
" c* ^+ ?0 ~; O所以,所谓的Magic Number,并不是从N元宇宙的某个星系由于时空扭曲而掉到地球上的,而是几百年前就有的数学理论。只要熟悉NR和泰勒级数,你我同样有能力作出类似的优化。
" C5 N+ p2 a9 i. i在GameDev.net上有人做过测试,该函数的相对误差约为0.177585%,速度比C标准库的sqrt提高超过20%。如果增加一次迭代过程,相对误差可以降低到e-004 的级数,但速度也会降到和sqrt差不多。据说在DOOM3中,Carmack通过查找表进一步优化了该算法,精度近乎完美,而且速度也比原版提高了一截(正在努力弄源码,谁有发我一份)。% d) V: X5 x7 h' u7 }, X9 X( r1 a* J' E
值得注意的是,在Chris Lomont的演算中,理论上最优秀的常数(精度最高)是0x5f37642f,并且在实际测试中,如果只使用一次迭代的话,其效果也是最好的。但奇怪的是,经过两次NR后,在该常数下解的精度将降低得非常厉害(天知道是怎么回事!)。经过实际的测试,Chris Lomont认为,最优秀的常数是0x5f375a86。如果换成64位的double版本的话,算法还是一样的,而最优常数则为0x5fe6ec85e7de30da(又一个令人冒汗的Magic Number - -b)。- y9 Y* e! M' r6 K4 S& G6 q  \
这个算法依赖于浮点数的内部表示和字节顺序,所以是不具移植性的。如果放到Mac上跑就会挂掉。如果想具备可移植性,还是乖乖用sqrt好了。但算法思想是通用的。大家可以尝试推算一下相应的平方根算法。
, s3 L. P# k7 i( G- y下面给出Carmack在QUAKE3中使用的平方根算法。Carmack已经将QUAKE3的所有源代码捐给开源了,所以大家可以放心使用,不用担心会受到律师信。
1 H) `; U, H5 a8 G6 Z* H0 B  K---------------------------------
" t1 C5 N: U7 Y/ T6 f//
( q( E/ x% W% w( K0 {+ k3 t5 y5 e! k// Carmack在QUAKE3中使用的计算平方根的函数
( J7 |. Z; I8 L8 L: C. [% X//! c' b7 z6 ?; F$ f7 E! |' O7 ?
float CarmSqrt(float x){
2 M$ [7 l7 \8 k) Q* X; M8 |union{. w% U$ r7 F9 o( \1 u& H
int intPart;! z7 P/ h0 ~; `8 g1 c
float floatPart;8 G! q0 ?( N& s7 [: e& y$ k
} convertor;" Z( s! w1 C5 ]* J4 i
union{
) j% S# W. C' C) ~int intPart;
1 O' }* w: s2 [" ^5 D+ W5 Nfloat floatPart;3 f( w4 ?7 S+ e# F+ C6 M' }* W
} convertor2;
6 Y* a$ e0 Q$ Yconvertor.floatPart = x;. ]  {) w, [; w9 h9 w
convertor2.floatPart = x;
3 K, k) n+ F9 ?convertor.intPart = 0x1FBCF800 + (convertor.intPart >> 1);0 D8 w7 m+ l2 G5 k* A4 R9 E
convertor2.intPart = 0x5f3759df - (convertor2.intPart >> 1);
* |' N4 W- M( W5 p, H6 _3 Xreturn 0.5f*(convertor.floatPart + (x * convertor2.floatPart));0 F& v0 x8 A) w3 Q" e
}
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的平方根的倒数' [$ l6 ?3 \/ J
    //
    " B# l8 A+ |. t, }& x6 [& Pfloat InvSqrt (float x)
    4 I5 l; L0 f2 a+ F{$ X% N$ ^7 U$ {8 a
    float xhalf = 0.5f*x;  N  I& k) e. p% V$ \5 R0 L
    int i = *(int*)&x;5 w$ i6 d- r, w
    i = 0x5f3759df - (i >> 1); // 计算第一个近似根( \: u) h& @9 z
    x = *(float*)&i;
    / J* |0 I6 K7 b: B7 {x = x*(1.5f - xhalf*x*x); // 牛顿迭代法: Z4 u/ ?5 p  i
    return x;4 }6 ~8 C+ I2 [% M& B0 }( \" Y% c
    }

    ! q  M* q/ \: ^4 W! U$ ^, T这个函数好像并不能实现计算x的平方根的倒数,比如我输入参数为100,它得到的却是-NAN
    生命,到最后总能成诗……
    回复

    使用道具 举报

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

    fastpost qq
    收缩
    • 电话咨询

    • 04714969085

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

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

    蒙公网安备 15010502000194号

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

    GMT+8, 2026-11-1 10:00 , Processed in 0.138260 second(s), 63 queries .

    回顶部