平方根倒數速算法

維基百科,自由的百科全書
遊戲實現光照和反射效果時以平方根倒數速算法計算波動角度,此圖以第一人稱射擊遊戲OpenArena爲例。

平方根倒數速算法英語Fast Inverse Square Root,亦常以“Fast InvSqrt()”或其使用的十六進制常數0x5f3759df代稱)是用於快速計算\scriptstyle x^{-\tfrac{1}{2}}(即\scriptstyle x平方根倒數,在此\scriptstyle x需取符合IEEE 754標準格式的32位浮點數)的一種算法。此算法最早可能是於90年代前期由SGI所發明,後來則於1999年在《雷神之錘III競技場》的源代碼中應用,但直到2002-2003年間纔在Usenet一類的公共論壇上出現[1]。這一算法的優勢在於減少了求平方根倒數時浮點運算操作帶來的巨大的運算耗費,而在計算機圖形學領域來說求取照明投影波動角度反射效果時就常需要計算平方根倒數。

此算法首先接收一個32位帶符浮點數,然後將之作爲一個32位整數看待,以將其向右進行一次邏輯移位的方式將之取半,並用十六進制“魔術數字”0x5f3759df減之,如此即可得對輸入的浮點數的平方根倒數的首次近似值;而後重新將其作爲浮點數,以牛頓法反覆迭代以求出更精確的近似值,直至求出符合精確度要求的近似值。以此算法計算浮點數的平方根倒數的同一精度的近似值比直接使用浮點數除法要快四倍。

此算法最早被認爲是由約翰·卡馬克所發明,但後來的調查顯示,該算法在這之前就於計算機圖形學的硬件與軟件領域有所應用,如SGI和3dfx就曾在產品中應用此算法。而就現在所知,此算法最早由Gary TarolliSGI Indigo的開發中使用。雖說在隨後的相關研究中也提出了一些可能的來源,但至今爲止仍未能確切知曉此常數的起源。

目錄

  [隱藏

[編輯]算法的切入點

法線常在光影效果實現計算時使用,而這就涉及到矢量範數的計算。圖中所標識的就是與一個面所垂直的一些矢量的集合。

浮點數的平方根倒數常用於計算正規化矢量[文 1]。3D圖形程序需要使用正規化矢量來實現光照和投影效果,因此每秒都需做上百萬次平方根倒數運算,而在處理座標轉換與光源的專用硬件設備出現前,這些計算都由軟件完成,計算速度亦相當之慢;在1990年代這段代碼開發出來之時,多數浮點數操作的速度更是遠遠滯後於整數操作[1],因而針對正規化矢量算法的優化就顯得尤爲重要。下面陳述計算正規化矢量的原理:

要將一個矢量標準化,就必須計算其歐幾里得範數以求得矢量長度,而這時就需對矢量的各分量的平方和求平方根;而當求取到其長度並以之除該矢量的每個分量後,所得的新矢量就是與原矢量同向的單位矢量,若以公式表示:

\|\boldsymbol{v}\| = \sqrt{v_1^2+v_2^2+v_3^2}可求得矢量v的歐幾里得範數,此算法正類如對歐幾里得空間的兩點求取其歐幾里得距離
\boldsymbol{\hat{v}} = \boldsymbol{v} / \|\boldsymbol{v}\|求得的就是標準化的矢量,若以\boldsymbol{x}代表v_1^2+v_2^2+v_3^2,則有\boldsymbol{\hat{v}} = \boldsymbol{v} / \sqrt{x}

可見標準化矢量時需要用到對矢量分量的平方根倒數計算,所以對平方根倒數計算算法的優化對計算正規化矢量也大有裨益。

爲了加速圖像處理單元計算,《雷神之錘III競技場》使用了平方根倒數速算法,而後來採用現場可編程邏輯門陣列頂點着色器也應用了此算法[文 2]

[編輯]代碼概覽

下列代碼是平方根倒數速算法在《雷神之錘III競技場》源代碼中的應用實例。示例剝離了C語言預處理器的指令,但附上了原有的註釋[2]

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(對浮點數的邪惡位級hack)
        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(第二次迭代,可以刪除)
 
        return y;
}

爲計算平方根倒數的值,軟件首先要先確定一個近似值,而後則使用某些數值方法不斷計算修改近似值,直至達到可接受的精度。在1990年代初(也即該算法發明的大概時間)軟件裏通用的平方根計算方法多是從查找表中取得近似值[文 3],而這段代碼取近似值耗時比之更短,達到精確度要求的速度也比通常使用的浮點除法計算法快四倍[文 4],雖然此算法會損失一些精度,但性能上的巨大優勢已足以補償損失的精度[文 5]。由代碼中對原數據的變量類型聲明爲float可看出,這一算法是針對的是IEEE 754標準格式的32位浮點數設計的,不過據Chris Lomont和後來的Charles McEniry的研究來看,這一算法也可套用於其他類型的浮點數上。

平方根倒數速算法在速度上的優勢源自將浮點數轉化爲長整型[注 1]以作整數看待,並用特定常數0x5f3759df與之相減。然而對於代碼閱讀者來說,他們卻難以立即領悟出使用這一常數的目的,因此這一常數和其它在代碼中出現的難以理解的常數同被稱爲“魔術數字[1][文 7][文 8][文 9]。如此將浮點數當作整數先位移後減法所得的浮點數結果即是對輸入數字的平方根倒數的粗略估計值,而後再進行一次牛頓迭代法以使之更精確後代碼即執行完畢。由於算法所生成的用於輸入牛頓法的首次近似值已經相當精確,所以此算法所計算出的近似值的精度已經可以接受,而若使用與《雷神之錘III競技場》同爲1999年發佈的Pentium III中的SSE指令rsqrtss計算,則計算平方根倒數的收斂速度更慢,精度也更低[4]

[編輯]將浮點數轉化爲整數

Float w significand.svg

要理解這段代碼,首先需瞭解浮點數的存儲格式。一個浮點數以32個二進制位表示一個有理數,而這32位由其意義分爲三段:首先首位爲符號位,如若是0則爲正數,反之爲負數;接下來的8位表示經過偏移處理(這是爲了使之能表示-127-128)後的指數;最後23位表示的則是有效數字中除最高位以外的其餘數字。將上述結構表示成公式即爲\scriptstyle x=(-1)^{\mathrm{Si}}\cdot(1+m)\cdot 2^{(E-B)},其中\scriptstyle m表示有效數字的尾數(此處\scriptstyle 0 \le m < 1,偏移量\scriptstyle B=127[文 10],而指數的值\scriptstyle E-B決定了有效數字(在Lomont和McEniry的論文中稱爲“尾數”(mantissa))代表的是小數還是整數[文 11]。以上圖爲例,將描述帶入有\scriptstyle m=1\times 2^{-2}=0.250),且\scriptstyle E-B=124-127=-3,則可得其表示的浮點數爲\scriptstyle x=(1+0.250)\cdot 2^{-3}=0.15625

符號位  
0 1 1 1 1 1 1 1 = 127
0 0 0 0 0 0 1 0 = 2
0 0 0 0 0 0 0 1 = 1
0 0 0 0 0 0 0 0 = 0
1 1 1 1 1 1 1 1 = −1
1 1 1 1 1 1 1 0 = −2
1 0 0 0 0 0 0 1 = −127
1 0 0 0 0 0 0 0 = −128
8位二進制整數補碼示例

如上所述,一個有符號正整數二進制補碼系統中的表示中首位爲0,而後面的各位則用於表示其數值。將浮點數取別名存儲爲整數時,該整數的數值即爲\scriptstyle I=E\times 2^{23}+M,其中E表示指數,M表示有效數字;若以上圖爲例,圖中樣例若作爲浮點數看待有\scriptstyle E=124M=1\cdot 2^{21},則易知其轉化而得的整數型號數值爲I=124\times 2^{23} + 2^{21}。由於平方根倒數函數僅能處理正數,因此浮點數的符號位(即如上的Si)必爲0,而這就保證了轉換所得的有符號整數也必爲正數。以上轉換就爲後面的計算帶來了可行性,之後的第一步操作(邏輯右移一位)即是使該數的長整形式被2所除[文 12]

[編輯]“魔術數字”

S(ign,符號) E(xponent,指數) M(antissa,尾數)
1 位 b位 (n-1-b)
n位[文 13]

對猜測平方根倒數速算法的最初構想來說,計算首次近似值所中使用的常數0x5f3759df也是重要的線索。爲確定程序員最初選此常數以近似求取平方根倒數的方法,Charles McEniry首先檢驗了在代碼中選擇任意常數R所求取出的首次近似值的精度。回想上一節關於整數和浮點數表示的比較:對於同樣的32位二進制數碼,若爲浮點數表示時實際數值爲\scriptstyle x=(1+m_x)2^{e_x},而若爲整數表示時實際數值則爲\scriptstyle I_x=E_xL+M_x[注 2],其中\scriptstyle L=2^{n-1-b}。以下等式引入了一些由指數和有效數字導出的新元素:

m_x=\frac{M_x}{L}
e_x=E_x-B,其中B=2^{b-1}-1

再繼續看McEniry 2007裏的進一步說明:

y=\frac{1}{\sqrt{x}}

對等式的兩邊取二進制對數\textstyle \log_2,即函數\textstyle f(n)=2^n反函數),有

\log_2{(y)}=-\frac{1}{2}\log_2{(x)}
\log_2(1+m_y)+e_y=-\frac{1}{2}\log_2{(1+m_x)}-\frac{1}{2}e_x

以如上方法就能將浮點數x和y的相關指數消去以將乘方運算化爲加法運算。而由於\scriptstyle \log_2{(x)}\scriptstyle \log_2{(x^{-1/2})}線性相關,因此在\scriptstyle x\scriptstyle y_0(即輸入值與首次近似值)間就可以線性組合的方式創建方程[文 10]。在此McEniry再度引入新數\sigma描述\scriptstyle \log_2{(1+x)}與近似值R間的誤差[注 3]:由於\scriptstyle 0 \le x < 1,有\scriptstyle \log_2{(1+x)}\approx {x},則在此可定義\sigma與x的關係爲\scriptstyle \log_2{(1+x)}\cong x+\sigma,這一定義就能提供二進制對數的首次精度值(此處0\le\sigma\le\tfrac{1}{3};當R爲0x5f3759df時,有\scriptstyle \sigma=0.0450461875791687011756[文 13])。由此將\scriptstyle \log_2{(1+x)}= x+\sigma代入上式,有:

m_y+\sigma+e_y=-\frac{1}{2}m_x-\frac{1}{2}\sigma-\frac{1}{2}e_x

參照首段等式代入M_xE_xBL,有:

M_y+(E_y-B)L=-\frac{3}{2}\sigma{L}-\frac{1}{2}M_x-\frac{1}{2}(E_x-B)L

移項整理得:

E_yL+M_y=\frac{3}{2}(B-\sigma)L-\frac{1}{2}(E_xL+M_x)

如上所述,對於以浮點規格存儲的正浮點數x,若將其作爲長整型表示則示值爲\textstyle I_x=E_xL+M_x,由此即可根據x的整數表示導出y(在此\textstyle y=\frac{1}{\sqrt{x}},亦即x的平方根倒數的首次近似值)的整數表示值,也即:

I_y=E_yL+M_y=R-\frac{1}{2}(E_xL+M_x)=R-\frac{1}{2}I_x,其中R=\frac{3}{2}(B-\sigma)L.

最後導出的等式\scriptstyle I_y=R-\frac{1}{2}I_x即與上節代碼中i = 0x5f3759df - (i>>1);一行相契合,由此可見平方根倒數速算法中對浮點數進行一次移位操作與整數減法就可以可靠地輸出一個浮點數的對應近似值[文 13]。到此爲止McEniry的證明只顯示出了可用常數R爲工具以近似求取浮點數的平方根倒數,但仍未能確定代碼中的R值的選取方法。

關於作一次移位與減法操作以使浮點數的指數被-2除的原理,Chris Lomont的論文中亦有個相對簡單的解釋:以\scriptstyle 10000=10^4爲例,將其指數除-2可得\scriptstyle 10000^{-1/2}=10^{-2}=1/100;而由於浮點表示的指數有進行過偏移處理,所以指數的真實值e應爲\scriptstyle e=E-127,因此可知除法操作的實際結果爲\scriptstyle -e/2+127[文 14],這時用R(在此即爲“魔術數字”0x5f3759df)減之即可使指數的最低有效數位轉入有效數字域,之後重新轉換爲浮點數時,就能得到一個相當接近所輸入的浮點數的平方根倒數的近似值。在這裏對常數R的選取亦有所講究,選取一個好的R值可以減少對指數進行除法與對有效數字域進行移位時可能產生的錯誤。基於這一標準,0xbe即是最合適的R值,而0xbe右移一位即可得到0x5f,這恰是魔術數字R的第一個字節[文 15]

[編輯]精確度

使用啓發式平方根倒數速算法與使用C語言標準庫libstdc的函數所計算出的平方根倒數的差值一覽,注意這裏使用的是雙對數座標系

如上所述,平方根倒數速算法所得的近似值驚人的精確,右圖亦展示了以上述代碼計算(以平方根倒數速算法計算後再進行一次牛頓法迭代)所得近似值的誤差:當輸入0.01時,以C語言標準庫函數計算可得10.0,而InvSqrt()得值爲9.9825822,其間誤差爲0.017479,相對誤差則爲0.175%,且當輸入更大的數值時,絕對誤差不斷下降,相對誤差也一直控制在一定的範圍之內。

[編輯]牛頓法

在進行了如上的整數操作之後,示例程序再度將被轉爲長整型的浮點數迴轉爲浮點數(對應x = *(float*)&i;)並對其進行一次浮點運算操作(對應x = x*(1.5f - xhalf*x*x);),這裏的浮點運算操作就是對其進行一次牛頓法迭代,若以此例說明:

y=\frac{1}{\sqrt{x}}所求的是y的平方根倒數,以之構造以y爲自變量的函數,有f(y)=\frac{1}{y^2}-x=0
將其代入牛頓法的通用公式y_{n+1} = y_{n} - \frac{f(y_n)}{f'(y_n)}(其中\, y_n爲首次近似值),
y_{n+1} = \frac{y_{n}(3-xy_n^2)}{2},其中f(y)=\frac{1}{y^2}-xf'(y)=\frac{-2}{y^3}
整理有\, y_{n+1} = \frac{y_{n}(3-xy_n^2)}{2} = y_{n}(1.5-\frac{xy_n^2}{2}),對應的代碼即爲x = x*(1.5f - xhalf*x*x);

在以上一節的整數操作產生首次近似值後,程序會將首次近似值作爲參數送入函數最後兩句進行精化處理,代碼中的兩次迭代(以一次迭代的輸出(對應公式中的y_{n+1})作爲二次迭代的輸入)正是爲了進一步提高結果的精度[文 16],但由於雷神之錘III引擎的圖形計算中並不需要太高的精度,所以代碼中只進行了一次迭代,二次迭代的代碼則被註釋[文 9]

[編輯]歷史與考究

id Software的創始人約翰·卡馬克。雖然這段代碼並非他所作,但他常被認爲與這段代碼相關。

《雷神之錘III》的代碼直到QuakeCon 2005才正式放出,但早在2002年(或2003年)時平方根倒數速算法的代碼就已經出現在Usenet與其他論壇上了[1]。最初人們猜測是卡馬克寫下了這段代碼,但他在詢問郵件的回覆中否定了這個觀點,並猜測可能是先前曾幫id Software優化雷神之錘的資深彙編程序員Terje Mathisen寫下了這段代碼;而在Mathisen的郵件裏他表示在1990年代初他只曾作過類似的實現,確切來說這段代碼亦非他所作。現在所知的最早實現是由Gary Tarilli在SGI Indigo中實現的,但他亦坦承他僅對常數R的取值做了一定的改進,實際上他也不是作者。Rys Sommefeldt則在向以發明MATLAB而聞名的Cleve Moler查證後認爲原始的算法是Ardent Computer公司的Greg Walsh所發明,但他也沒有任何決定性的證據能證明這一點[5]

現在不僅該算法的原作者不明,人們也仍無法明確當初選擇這個“魔術數字”的方法。Chris Lomont在研究中曾做了個試驗:他編寫了一個函數,以在一個範圍內遍歷選取R值的方式將逼近誤差降到最小,以此方法他計算出了線性近似的最優R值0x5f37642f(與代碼中使用的0x5f3759df相當接近),但以之代入算法計算並進行一次牛頓迭代後,所得近似值與代入0x5f3759df的結果相比精度卻仍略微更低[文 17];而後Lomont將目標改爲遍歷選取在進行1-2次牛頓迭代後能得到最大精度的R值,並由此算出最優R值爲0x5f375a86,以此值代入算法並進行牛頓迭代後所得的結果都比代入原始值(0x5f3759df)更精確[文 17],於是他的論文最後以“原始常數是以數學推導還是以反覆試錯的方式求得”的問題作結[文 18]。在論文中Lomont亦指出64位的IEEE754浮點數(即雙精度類型)所對應的魔術數字是0x5fe6ec85e7de30da,但後來的研究表明代入0x5fe6eb50c7aa19f9的結果精確度更高(McEniry得出的結果則是0x5FE6EB50C7B537AA,精度介於兩者之間)。在Charles McEniry的論文中,他使用了一種類似Lomont但更復雜的方法來優化R值:他最開始使用窮舉搜索,所得結果與Lomont相同[文 19];而後他嘗試用帶權二分法尋找最優值,所得結果恰是代碼中所使用的魔術數字0x5f3759df,因此McEniry確信這一常數或許最初便是以“在可容忍誤差範圍內使用二分法”的方式求得[文 20]

[編輯]註釋

  1. ^ 由於現代計算機系統對長整型的定義有所差異,使用長整型會降低此段代碼的可移植性。具體來說,由此段浮點轉換爲長整型的定義可知,如若這段代碼正常運行,所在系統的長整型長度應爲4字節(32位),否則重新轉爲浮點數時可能會變成負數;而由於C99標準的廣泛應用,在現今多數64位計算機系統(除使用LLP64數據模型的Windows外)中,長整型的長度都是8字節[文 6][3]
  2. ^ 此處“浮點數”所指爲標準化浮點數,也即有效數字部分必須滿足\scriptstyle 0 \le m_x <1),可參見David Goldberg. What Every Computer Scientist Should Know About Floating-Point Arithmetic. ACM Computing Surveys. 1991.March, 23(1): 5–48. doi:10.1145/103162.103163.
  3. ^ Lomont 2003確定R的方式則有所不同,他先將R分解爲\scriptstyle R_1\scriptstyle R_2,其中\scriptstyle R_1\scriptstyle R_2分別代表R的有效數字域和指數域[文 7]

[編輯]參考

[編輯]腳註

  1. 1.0 1.1 1.2 1.3 Sommefeldt, Rys. Origin of Quake3's Fast InvSqrt(). Beyond3D. 2006-11-29 [2009-02-12] (英文).
  2. ^ quake3-1.32b/code/game/q_math.cQuake III Arenaid Software[2010-11-15].
  3. ^ Meyers, Randy. The New C: Integers in C99, Part 1. drdobbs.com. 2000-12-01 [2010-09-04].
  4. ^ Ruskin, Elan. Timing square root. Some Assembly Required. 2009-10-16 [2010-09-13].
  5. ^ Sommefeldt, Rys. Origin of Quake3's Fast InvSqrt() - Part Two. Beyond3D. 2006-12-19 [2008-04-19].
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章