注:我(轉(zhuǎn)載本文的人)實(shí)測(cè)結(jié)果是sqrt()函數(shù)要比卡馬克的方法更快一些,測(cè)試環(huán)境xp sp2+ vc6 + stlport
-------------------------------------------------以下是轉(zhuǎn)載原文
好吧,我承認(rèn)我標(biāo)題黨了,不過(guò)既然你來(lái)了,就認(rèn)真看下去吧,保證你有收獲。
我們平時(shí)經(jīng)常會(huì)有一些數(shù)據(jù)運(yùn)算的操作,需要調(diào)用sqrt,exp,abs等函數(shù),那么時(shí)候你有沒(méi)有想過(guò):這個(gè)些函數(shù)系統(tǒng)是如何實(shí)現(xiàn)的?就拿最常用的sqrt函數(shù)來(lái)說(shuō)吧,系統(tǒng)怎么來(lái)實(shí)現(xiàn)這個(gè)經(jīng)常調(diào)用的函數(shù)呢?
雖然有可能你平時(shí)沒(méi)有想過(guò)這個(gè)問(wèn)題,不過(guò)正所謂是“臨陣磨槍?zhuān)豢煲补?#8221;,你“眉頭一皺,計(jì)上心來(lái)”,這個(gè)不是太簡(jiǎn)單了嘛,用二分的方法,在一個(gè)區(qū)間中,每次拿中間數(shù)的平方來(lái)試驗(yàn),如果大了,就再試左區(qū)間的中間數(shù);如果小了,就再拿右區(qū)間的中間數(shù)來(lái)試。比如求sqrt(16)的結(jié)果,你先試(0+16)/2=8,8*8=64,64比16大,然后就向左移,試(0+8)/2=4,4*4=16剛好,你得到了正確的結(jié)果sqrt(16)=4。然后你三下五除二就把程序?qū)懗鰜?lái)了:
float SqrtByBisection(float n) //用二分法
{
if(n<0) //小于0的按照你需要的處理
return n;
float mid,last;
float low,up;
low=0,up=n;
mid=(low+up)/2;
do
{
if(mid*mid>n)
up=mid;
else
low=mid;
last=mid;
mid=(up+low)/2;
}while(abs(mid-last) > eps);//精度控制
return mid;
}
然后看看和系統(tǒng)函數(shù)性能和精度的差別(其中時(shí)間單位不是秒也不是毫秒,而是CPU Tick,不管單位是什么,統(tǒng)一了就有可比性)

從圖中可以看出,二分法和系統(tǒng)的方法結(jié)果上完全相同,但是性能上整整差了幾百倍。為什么會(huì)有這么大的區(qū)別呢?難道系統(tǒng)有什么更好的辦法?難道。。。。哦,對(duì)了,回憶下我們?cè)?jīng)的高數(shù)課,曾經(jīng)老師教過(guò)我們“牛頓迭代法快速尋找平方根”,或者這種方法可以幫助我們,具體步驟如下:
求出根號(hào)a的近似值:首先隨便猜一個(gè)近似值x,然后不斷令x等于x和a/x的平均數(shù),迭代個(gè)六七次后x的值就已經(jīng)相當(dāng)精確了。
例如,我想求根號(hào)2等于多少。假如我猜測(cè)的結(jié)果為4,雖然錯(cuò)的離譜,但你可以看到使用牛頓迭代法后這個(gè)值很快就趨近于根號(hào)2了:
( 4 + 2/4 ) / 2 = 2.25
( 2.25 + 2/2.25 ) / 2 = 1.56944..
( 1.56944..+ 2/1.56944..) / 2 = 1.42189..
( 1.42189..+ 2/1.42189..) / 2 = 1.41423..
....
這種算法的原理很簡(jiǎn)單,我們僅僅是不斷用(x,f(x))的切線來(lái)逼近方程x^2-a=0的根。根號(hào)a實(shí)際上就是x^2-a=0的一個(gè)正實(shí)根,這個(gè)函數(shù)的導(dǎo)數(shù)是2x。也就是說(shuō),函數(shù)上任一點(diǎn)(x,f(x))處的切線斜率是2x。那么,x-f(x)/(2x)就是一個(gè)比x更接近的近似值。代入 f(x)=x^2-a得到x-(x^2-a)/(2x),也就是(x+a/x)/2。
相關(guān)的代碼如下:
float SqrtByNewton(float x)
{
float val = x;//最終
float last;//保存上一個(gè)計(jì)算的值
do
{
last = val;
val =(val + x/val) / 2;
}while(abs(val-last) > eps);
return val;
}
然后我們?cè)賮?lái)看下性能測(cè)試:

哇塞,性能提高了很多,可是和系統(tǒng)函數(shù)相比,還是有這么大差距,這是為什么呀?想啊想啊,想了很久仍然百思不得其解。突然有一天,我在網(wǎng)上看到一個(gè)神奇的方法,于是就有了今天的這篇文章,廢話不多說(shuō),看代碼先:
float InvSqrt(float x)
{
float xhalf = 0.5f*x;
int i = *(int*)&x; // get bits for floating VALUE
i = 0x5f375a86- (i>>1); // gives initial guess y0
x = *(float*)&i; // convert bits BACK to float
x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
return 1/x;
}
然后我們最后一次來(lái)看下性能測(cè)試:
這次真的是質(zhì)變了,結(jié)果竟然比系統(tǒng)的還要好。。。哥真的是震驚了!!!哥吐血了!!!一個(gè)函數(shù)引發(fā)了血案!!!血案,血案。。。
到現(xiàn)在你是不是還不明白那個(gè)“鬼函數(shù)”,到底為什么速度那么快嗎?不急,先看看下面的故事吧:
Quake-III Arena (雷神之錘3)是90年代的經(jīng)典游戲之一。該系列的游戲不但畫(huà)面和內(nèi)容不錯(cuò),而且即使計(jì)算機(jī)配置低,也能極其流暢地運(yùn)行。這要?dú)w功于它3D引擎的開(kāi)發(fā)者約翰-卡馬克(John Carmack)。事實(shí)上早在90年代初DOS時(shí)代,只要能在PC上搞個(gè)小動(dòng)畫(huà)都能讓人驚嘆一番的時(shí)候,John Carmack就推出了石破天驚的Castle Wolfstein, 然后再接再勵(lì),doom, doomII, Quake...每次都把3-D技術(shù)推到極致。他的3D引擎代碼資極度高效,幾乎是在壓榨PC機(jī)的每條運(yùn)算指令。當(dāng)初MS的Direct3D也得聽(tīng)取他的意見(jiàn),修改了不少API。
最近,QUAKE的開(kāi)發(fā)商ID SOFTWARE 遵守GPL協(xié)議,公開(kāi)了QUAKE-III的原代碼,讓世人有幸目睹Carmack傳奇的3D引擎的原碼。這是QUAKE-III原代碼的下載地址:
http://www.fileshack.com/file.x?fid=7547
(下面是官方的下載網(wǎng)址,搜索 “quake3-1.32b-source.zip” 可以找到一大堆中文網(wǎng)頁(yè)的。ftp://ftp.idsoftware.com/idstuff/source/quake3-1.32b-source.zip)
我們知道,越底層的函數(shù),調(diào)用越頻繁。3D引擎歸根到底還是數(shù)學(xué)運(yùn)算。那么找到最底層的數(shù)學(xué)運(yùn)算函數(shù)(在game/code/q_math.c), 必然是精心編寫(xiě)的。里面有很多有趣的函數(shù),很多都令人驚奇,估計(jì)我們幾年時(shí)間都學(xué)不完。在game/code/q_math.c里發(fā)現(xiàn)了這樣一段代碼。它的作用是將一個(gè)數(shù)開(kāi)平方并取倒,經(jīng)測(cè)試這段代碼比(float)(1.0/sqrt(x))快4倍:
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
#ifndef Q3_VM
#ifdef __linux__
assert( !isnan(y) ); // bk010122 - FPE?
#endif
#endif
return y;
}
函數(shù)返回1/sqrt(x),這個(gè)函數(shù)在圖像處理中比sqrt(x)更有用。
注意到這個(gè)函數(shù)只用了一次疊代!(其實(shí)就是根本沒(méi)用疊代,直接運(yùn)算)。編譯,實(shí)驗(yàn),這個(gè)函數(shù)不僅工作的很好,而且比標(biāo)準(zhǔn)的sqrt()函數(shù)快4倍!要知道,編譯器自帶的函數(shù),可是經(jīng)過(guò)嚴(yán)格仔細(xì)的匯編優(yōu)化的啊!
這個(gè)簡(jiǎn)潔的函數(shù),最核心,也是最讓人費(fèi)解的,就是標(biāo)注了“what the fuck?”的一句
i = 0x5f3759df - ( i >> 1 );
再加上y = y * ( threehalfs - ( x2 * y * y ) );
兩句話就完成了開(kāi)方運(yùn)算!而且注意到,核心那句是定點(diǎn)移位運(yùn)算,速度極快!特別在很多沒(méi)有乘法指令的RISC結(jié)構(gòu)CPU上,這樣做是極其高效的。
算法的原理其實(shí)不復(fù)雜,就是牛頓迭代法,用x-f(x)/f'(x)來(lái)不斷的逼近f(x)=a的根。
沒(méi)錯(cuò),一般的求平方根都是這么循環(huán)迭代算的但是卡馬克(quake3作者)真正牛B的地方是他選擇了一個(gè)神秘的常數(shù)0x5f3759df 來(lái)計(jì)算那個(gè)猜測(cè)值,就是我們加注釋的那一行,那一行算出的值非常接近1/sqrt(n),這樣我們只需要2次牛頓迭代就可以達(dá)到我們所需要的精度。好吧如果這個(gè)還不算NB,接著看:
普渡大學(xué)的數(shù)學(xué)家Chris Lomont看了以后覺(jué)得有趣,決定要研究一下卡馬克弄出來(lái)的這個(gè)猜測(cè)值有什么奧秘。Lomont也是個(gè)牛人,在精心研究之后從理論上也推導(dǎo)出一個(gè)最佳猜測(cè)值,和卡馬克的數(shù)字非常接近, 0x5f37642f。卡馬克真牛,他是外星人嗎?
傳奇并沒(méi)有在這里結(jié)束。Lomont計(jì)算出結(jié)果以后非常滿意,于是拿自己計(jì)算出的起始值和卡馬克的神秘?cái)?shù)字做比賽,看看誰(shuí)的數(shù)字能夠更快更精確的求得平方根。結(jié)果是卡馬克贏了... 誰(shuí)也不知道卡馬克是怎么找到這個(gè)數(shù)字的。
最后Lomont怒了,采用暴力方法一個(gè)數(shù)字一個(gè)數(shù)字試過(guò)來(lái),終于找到一個(gè)比卡馬克數(shù)字要好上那么一丁點(diǎn)的數(shù)字,雖然實(shí)際上這兩個(gè)數(shù)字所產(chǎn)生的結(jié)果非常近似,這個(gè)暴力得出的數(shù)字是0x5f375a86。
Lomont為此寫(xiě)下一篇論文,"Fast Inverse Square Root"。 論文下載地址:
http://www.math.purdue.edu/~clomont/Math/Papers/2003/InvSqrt.pdf
http://www.matrix67.com/data/InvSqrt.pdf
參考:<IEEE Standard 754 for Binary Floating-Point Arithmetic><FAST INVERSE SQUARE ROOT>
最后,給出最精簡(jiǎn)的1/sqrt()函數(shù):
float InvSqrt(float x)
{
float xhalf = 0.5f*x;
int i = *(int*)&x; // get bits for floating VALUE
i = 0x5f375a86- (i>>1); // gives initial guess y0
x = *(float*)&i; // convert bits BACK to float
x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
return x;
}
大家可以嘗試在PC機(jī)、51、AVR、430、ARM、上面編譯并實(shí)驗(yàn),驚訝一下它的工作效率。
前兩天有一則新聞,大意是說(shuō) Ryszard Sommefeldt 很久以前看到這么樣的一段 code (可能出自 Quake III 的 source code):
float InvSqrt (float x)
{
float xhalf = 0.5f*x;
int i = *(int*)&x;
i = 0x5f3759df - (i>>1);
x = *(float*)&i;
x = x*(1.5f - xhalf*x*x);
return x;
}
他一看之下驚為天人,想要拜見(jiàn)這位前輩高人,但是一路追尋下去卻一直找不到人;同時(shí)間也有其他人在找,雖然也沒(méi)找到出處,但是 Chris Lomont 寫(xiě)了一篇論文 (in PDF) 解析這段 code 的算法 (用的是 Newton’s Method,牛頓法;比較重要的是后半段講到怎么找出神奇的 0x5f3759df 的)。
PS. 這個(gè) function 之所以重要,是因?yàn)榍?開(kāi)根號(hào)倒數(shù) 這個(gè)動(dòng)作在 3D 運(yùn)算 (向量運(yùn)算的部份) 里面常常會(huì)用到,如果你用最原始的 sqrt() 然后再倒數(shù)的話,速度比上面的這個(gè)版本大概慢了四倍吧… XD
PS2. 在他們追尋的過(guò)程中,有人提到一份叫做 MIT HACKMEM 的文件,這是 1970 年代的 MIT 強(qiáng)者們做的一些筆記 (hack memo),大部份是 algorithm,有些 code 是 PDP-10 asm 寫(xiě)的,另外有少數(shù)是 C code (有人整理了一份列表)
好了,故事就到這里結(jié)束了,希望大家能有有收獲:)
下載代碼
TestSqrt.cpp (2.58 kb)