天天看點

日學一算法---平方根倒數速算法【轉】

本文來源:http://blog.csdn.net/xiaoguohaha/article/details/21652643

文章太牛了,裡面的算法更是讓我膜拜萬分,不得不轉過來收藏一下。

     我們平時經常會有一些資料運算的操作,需要調用sqrt,exp,abs等函數,那麼時候你有沒有想過:這個些函數系統是如何實作的?就拿最常用的sqrt函數來說吧,系統怎麼來實作這個經常調用的函數呢?

     雖然有可能你平時沒有想過這個問題,不過正所謂是“臨陣磨槍,不快也光”,你“眉頭一皺,計上心來”,這個不是太簡單了嘛,用二分的方法,在一個區間中,每次拿中間數的平方來試驗,如果大了,就再試左區間的中間數;如果小了,就再拿右區間的中間數來試。比如求sqrt(16)的結果,你先試(0+16)/2=8,8*8=64,64比16大,然後就向左移,試(0+8)/2=4,4*4=16剛好,你得到了正确的結果sqrt(16)=4。然後你三下五除二就把程式寫出來了:

1 //用二分法
 2 float SqrtByBisection(float n)
 3 {
 4     //小于0的按照你需要的處理
 5     if(n < 0)
 6         return n;
 7     float mid,last;
 8     float low,up;
 9     low=0,up=n;
10     mid=(low+up)/2;
11     do
12     {
13         if(mid*mid>n)
14             up=mid;
15         else
16             low=mid;
17         last=mid;
18         mid=(up+low)/2;
19     }
20     //精度控制
21     while(abs(mid-last) > eps);
22     return mid;
23 }      

View Code

     然後看看和系統函數性能和精度的差别(其中時間機關不是秒也不是毫秒,而是CPU Tick,不管機關是什麼,統一了就有可比性)。二分法和系統的方法結果上完全相同,但是性能上整整差了幾百倍。為什麼會有這麼大的差別呢?難道系統有什麼更好的辦法?難道。。。。哦,對了,回憶下我們曾經的高數課,曾經老師教過我們“牛頓疊代法快速尋找平方根”,或者這種方法可以幫助我們,具體步驟如下。

求出根号a的近似值:首先随便猜一個近似值x,然後不斷令x等于x和a/x的平均數,疊代個六七次後x的值就已經相當精确了。例如,我想求根号2等于多少。假如我猜測的結果為4,雖然錯的離譜,但你可以看到使用牛頓疊代法後這個值很快就趨近于根号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..

....

這種算法的原理很簡單,我們僅僅是不斷用(x,f(x))的切線來逼近方程x^2-a=0的根。根号a實際上就是x^2-a=0的一個正實根,這個函數的導數是2x。也就是說,函數上任一點(x,f(x))處的切線斜率是2x。那麼,x-f(x)/(2x)就是一個比x更接近的近似值。代入 f(x)=x^2-a得到x-(x^2-a)/(2x),也就是(x+a/x)/2。

相關的代碼如下: 

1 float SqrtByNewton(float x)
 2 {
 3     // 最終
 4     float val = x;
 5     // 儲存上一個計算的值
 6     float last;
 7     do
 8     {
 9         last = val;
10         val =(val + x/val) / 2;
11     }
12     while(abs(val-last) > eps);
13     return val;
14 }      

View Code

     牛頓疊代法性能提高了很多,可是和系統函數相比,還是有這麼大差距,這是為什麼呀?想啊想啊,想了很久仍然百思不得其解。突然有一天,我在網上看到一個神奇的方法,于是就有了今天的這篇文章,廢話不多說,看代碼先:

1 float InvSqrt(float x)
 2 {
 3     float xhalf = 0.5f*x;
 4     int i = *(int*)&x; // get bits for floating VALUE
 5     i = 0x5f375a86- (i>>1); // gives initial guess y0
 6     x = *(float*)&i; // convert bits BACK to float
 7     x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
 8     x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
 9     x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
10 
11     return 1/x;
12 }      

View Code

這次真的是質變了,結果竟然比系統的還要好。到現在你是不是還不明白那個“鬼函數”,到底為什麼速度那麼快嗎?不急,先看看下面的故事吧:

Quake-III Arena (雷神之錘3)是90年代的經典遊戲之一。該系列的遊戲不但畫面和内容不錯,而且即使計算機配置低,也能極其流暢地運作。這要歸功于它3D引擎的開發者約翰-卡馬克(John Carmack)。事實上早在90年代初DOS時代,隻要能在PC上搞個小動畫都能讓人驚歎一番的時候,John Carmack就推出了石破天驚的Castle Wolfstein, 然後再接再勵,doom, doomII, Quake...每次都把3-D技術推到極緻。他的3D引擎代碼資極度高效,幾乎是在壓榨PC機的每條運算指令。當初MS的Direct3D也得聽取他的意見,修改了不少API。

最近,QUAKE的開發商ID SOFTWARE 遵守GPL協定,公開了QUAKE-III的原代碼,讓世人有幸目睹Carmack傳奇的3D引擎的原碼。這是QUAKE-III原代碼的下載下傳位址: http://www.fileshack.com/file.x?fid=7547。我們知道,越底層的函數,調用越頻繁。3D引擎歸根到底還是數學運算。那麼找到最底層的數學運算函數(在game/code/q_math.c), 必然是精心編寫的。裡面有很多有趣的函數,很多都令人驚奇,估計我們幾年時間都學不完。在game/code/q_math.c裡發現了這樣一段代碼。它的作用是将一個數開平方并取倒,經測試這段代碼比(float)(1.0/sqrt(x))快4倍:

1 float Q_rsqrt( float number )
 2 {
 3     long i;
 4     float x2, y;
 5     const float threehalfs = 1.5F;
 6 
 7     x2 = number * 0.5F;
 8     y   = number;
 9     i   = * ( long * ) &y;   // evil floating point bit level hacking
10     i   = 0x5f3759df - ( i >> 1 ); // what the fuck?
11     y   = * ( float * ) &i;
12     y   = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
13     // y   = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
14 
15     #ifndef Q3_VM
16     #ifdef __linux__
17          assert( !isnan(y) ); // bk010122 - FPE?
18     #endif
19     #endif
20     return y;
21 }       

View Code

函數傳回1/sqrt(x),這個函數在圖像進行中比sqrt(x)更有用。注意到這個函數隻用了一次疊代!(其實就是根本沒用疊代,直接運算)。編譯,實驗,這個函數不僅工作的很好,而且比标準的sqrt()函數快4倍!要知道,編譯器自帶的函數,可是經過嚴格仔細的彙編優化的啊!

這個簡潔的函數,最核心,也是最讓人費解的,就是标注了“what the fuck?”的一句:i = 0x5f3759df - ( i >> 1 );

再加上y = y * ( threehalfs - ( x2 * y * y ) );

兩句話就完成了開方運算!而且注意到,核心那句是定點移位運算,速度極快!特别在很多沒有乘法指令的RISC結構CPU上,這樣做是極其高效的。

算法的原理其實不複雜,就是牛頓疊代法,用x-f(x)/f'(x)來不斷的逼近f(x)=a的根。

沒錯,一般的求平方根都是這麼循環疊代算的但是卡馬克(quake3作者)真正牛B的地方是他選擇了一個神秘的常數0x5f3759df 來計算那個猜測值,就是我們加注釋的那一行,那一行算出的值非常接近1/sqrt(n),這樣我們隻需要2次牛頓疊代就可以達到我們所需要的精度。好吧如果這個還不算NB,接着看:

普渡大學的數學家Chris Lomont看了以後覺得有趣,決定要研究一下卡馬克弄出來的這個猜測值有什麼奧秘。Lomont也是個牛人,在精心研究之後從理論上也推導出一個最佳猜測值,和卡馬克的數字非常接近, 0x5f37642f。卡馬克真牛,他是外星人嗎?

傳奇并沒有在這裡結束。Lomont計算出結果以後非常滿意,于是拿自己計算出的起始值和卡馬克的神秘數字做比賽,看看誰的數字能夠更快更精确的求得平方根。結果是卡馬克赢了... 誰也不知道卡馬克是怎麼找到這個數字的。

最後Lomont怒了,采用暴力方法一個數字一個數字試過來,終于找到一個比卡馬克數字要好上那麼一丁點的數字,雖然實際上這兩個數字所産生的結果非常近似,這個暴力得出的數字是0x5f375a86。

Lomont為此寫下一篇論文,"Fast Inverse Square Root"。 論文下載下傳位址:http://www.math.purdue.edu/~clomont/Math/Papers/2003/InvSqrt.pdf ,http://www.matrix67.com/data/InvSqrt.pdf。

最後,給出最精簡的1/sqrt()函數:

1 float InvSqrt(float x)
2 {
3     float xhalf = 0.5f*x;
4     int i = *(int*)&x; // get bits for floating VALUE
5     i = 0x5f375a86- (i>>1); // gives initial guess y0
6     x = *(float*)&i; // convert bits BACK to float
7     x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
8     return x;
9 }      

View Code

大家可以嘗試在PC機、51、AVR、430、ARM、上面編譯并實驗,驚訝一下它的工作效率。

前兩天有一則新聞,大意是說 Ryszard Sommefeldt 很久以前看到這麼樣的一段 code (可能出自 Quake III 的 source code):

1 float InvSqrt (float x)
 2 {
 3     float xhalf = 0.5f*x;
 4     int i = *(int*)&x;
 5     i = 0x5f3759df - (i>>1);
 6     x = *(float*)&i;
 7     x = x*(1.5f - xhalf*x*x);
 8     return x;
 9 }
10        

View Code

他一看之下驚為天人,想要拜見這位前輩高人,但是一路追尋下去卻一直找不到人;同時間也有其他人在找,雖然也沒找到出處,但是 Chris Lomont 寫了一篇論文 (in PDF) 解析這段 code 的算法 (用的是 Newton’s Method,牛頓法;比較重要的是後半段講到怎麼找出神奇的 0x5f3759df 的)。

PS. 這個 function 之是以重要,是因為求 開根号倒數 這個動作在 3D 運算 (向量運算的部份) 裡面常常會用到,如果你用最原始的 sqrt() 然後再倒數的話,速度比上面的這個版本大概慢了四倍吧… XD

PS2. 在他們追尋的過程中,有人提到一份叫做 MIT HACKMEM 的檔案,這是 1970 年代的 MIT 強者們做的一些筆記 (hack memo),大部份是 algorithm,有些 code 是 PDP-10 asm 寫的,另外有少數是 C code (有人整理了一份清單)。

好了,故事就到這裡結束了,希望大家能有有收獲:)

原文轉自:http://www.nowamagic.net/algorithm/algorithm_EfficacyOfFunctionSqrt.php

說實話,經過測試,本文所提到的計算開平方的算法在精度方面跟sqrt相比還是有差距的。

測試時,我把精度定為1e-7,分别用兩個算法計算了1!10000的開平方根,發現很多數

經兩者開根号結果是不一樣的。

下面是測試代碼:

1 #include<stdio.h>
 2 #include<math.h>
 3 double delta=1e-7;
 4 float InvSqrt(float x);
 5 int comparDoubleNum(double x,double y);
 6 int main()
 7 {
 8     int i;
 9     double ans1,ans2;
10     freopen("out.txt","w",stdout);
11     for(i=1;i<=10000;i++)
12     {
13         ans1=sqrt(i);
14         ans2=InvSqrt(i);
15         printf("%d---%lf---%lf---  %d\n",i,ans1,ans2,comparDoubleNum(ans1,ans2));
16     }
17     return 0;
18 }
19 float InvSqrt(float x)
20 {
21     float xhalf = 0.5f*x;
22     int i = *(int*)&x; // get bits for floating VALUE
23     i = 0x5f375a86- (i>>1); // gives initial guess y0
24     x = *(float*)&i; // convert bits BACK to float
25     x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
26     x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
27     x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
28 
29     return 1/x;
30 }
31 int comparDoubleNum(double x,double y)//當x與y之差的絕對值小于0.00001(即:1e-5)時 認為x等于y 
32 {
33     if((x-y)>delta) return 1;
34     else if((x-y)<-delta) return -1;
35     else return 0;
36 }      

View Code

轉載于:https://www.cnblogs.com/huashanqingzhu/p/3615543.html

繼續閱讀