反平方根快速演算法

用來求某個實數的平方根的倒數的演算法

反平方根快速演算法(英語:Fast Inverse Square Root,亦常以「Fast InvSqrt()」或其使用的十六進制常數0x5f3759df代稱)是用於快速計算(即平方根倒數,在此需取符合IEEE 754標準格式的32位元浮點數)的一種演算法。這一演算法的優勢在於減少了求平方根倒數時浮點運算操作帶來的巨大的運算耗費,而在電腦圖學領域,若要求取照明投影波動角度反射效果,就常需計算平方根倒數。

遊戲實現光照和反射效果時以反平方根快速演算法計算波動角度,此圖以第一人稱射擊遊戲OpenArena為例。

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

此演算法最早被認為是由約翰·卡馬克於90年代前期在SGI Indigo英語SGI Indigo的開發中使用,後來則於1999年在《雷神之鎚III競技場》的原始碼中應用,但直到2002-2003年間才在Usenet一類的公共討論區上出現[1]。後來的調查顯示,該演算法在這之前就於電腦圖學的硬件與軟件領域有所應用,如SGI3dfx就曾在產品中應用此演算法,但至今為止仍未能確切知曉演算法中所使用的特殊常數的起源。

演算法的切入點

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

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

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

 可求得向量v的歐幾里得範數,此演算法正類如對歐幾里得空間的兩點求取其歐幾里得距離
 求得的就是標準化的向量,若以 代表 ,則有 

可見標準化向量時,對向量分量計算平方根倒數實為必需,所以,對平方根倒數計算演算法的最佳化對計算正規化向量也大有裨益。

為了加速圖像處理單元計算,《雷神之鎚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(邪恶的浮点数位运算黑科技)
	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年代初(也即該演算法發明的大概時間),軟件開發時通用的平方根演算法英語Methods of computing square roots多是從尋找表中取得近似值[文 3],而這段代碼取近似值耗時比之更短,達到精確度要求的速度也比通常使用的浮點除法計演算法快四倍[文 4],雖然此演算法會損失一些精度,但效能上的巨大優勢已足以補償損失的精度[文 5]。由代碼中對原數據的變數類型聲明為float可看出,這一演算法是針對IEEE 754標準格式的32位元浮點數設計的,不過據Chris Lomont和後來的Charles McEniry的研究看,這一演算法亦可套用於其他類型的浮點數上。

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

將浮點數轉化為整數

 

要理解這段代碼,首先需了解浮點數的儲存格式。一個浮點數以32個位元表示一個有理數,而這32位元由其意義分為三段:首先首位為符號位,如若是0則為正數,反之為負數;接下來的8位元表示經過偏移處理(這是為了使之能表示-127-128)後的指數;最後23位表示的則是有效數字中除最高位以外的其餘數字。將上述結構表示成公式即為 ,其中 表示有效數字的尾數(此處 ,偏移量 [文 10],而指數的值 決定了有效數字(在Lomont和McEniry的論文中稱為「尾數」(mantissa))代表的是小數還是整數[文 11]。以上圖為例,將描述帶入有 ),且 ,則可得其表示的浮點數為 

符號位
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,而後面的各位則用於表示其數值。將浮點數取別名英語Aliasing (computing)儲存為整數時,該整數的數值即為 ,其中E表示指數,M表示有效數字;若以上圖為例,圖中樣例若作為浮點數看待有  ,則易知其轉化而得的整數型號數值為 。由於平方根倒數函數僅能處理正數,因此浮點數的符號位(即如上的Si)必為0,而這就保證了轉換所得的有符號整數也必為正數。以上轉換就為後面的計算帶來了可行性,之後的第一步操作(邏輯右移一位)即是使該數的長整形式被2所除[文 12]

「魔術數字」

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

對猜測反平方根快速演算法的最初構想來說,計算首次近似值所使用的常數0x5f3759df也是重要的線索。為確定程式設計師最初選此常數以近似求取平方根倒數的方法,Charles McEniry首先檢驗了在代碼中選擇任意常數R所求取出的首次近似值的精度。回想上一節關於整數和浮點數表示的比較:對於同樣的32位元二進制數碼,若為浮點數表示時實際數值為 ,而若為整數表示時實際數值則為 [註 2],其中 。以下等式引入了一些由指數和有效數字匯出的新元素:

 
 ,其中 

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

 

對等式的兩邊取二進制對數 ,即函數 反函數),有

 
 

以如上方法,就能將浮點數x和y的相關指數消去,從而將乘方運算化為加法運算。而由於  線性相關,因此在  (即輸入值與首次近似值)間就可以線性組合的方式建立方程[文 10]。在此McEniry再度引入新數 描述 與近似值R間的誤差[註 3]:由於 ,有 ,則在此可定義 與x的關係為 ,這一定義就能提供二進制對數的首次精度值(此處 ;當R為0x5f3759df時,有 [文 13])。由此將 代入上式,有:

 

參照首段等式代入    ,有:

 

移項整理得:

 

如上所述,對於以浮點規格儲存的正浮點數x,若將其作為長整型表示則示值為 ,由此即可根據x的整數表示匯出y(在此 ,亦即x的平方根倒數的首次近似值)的整數表示值,也即:

 ,其中 .

最後匯出的等式 即與上節代碼中i = 0x5f3759df - (i>>1);一行相契合,由此可見,在反平方根快速演算法中,對浮點數進行一次移位元運算與整數減法,就可以可靠地輸出一個浮點數的對應近似值[文 13]。到此為止,McEniry只證明了,在常數R的輔助下,可近似求取浮點數的平方根倒數,但仍未能確定代碼中的R值的選取方法。

關於作一次移位與減法操作以使浮點數的指數被-2除的原理,Chris Lomont的論文中亦有個相對簡單的解釋:以 為例,將其指數除-2可得 ;而由於浮點表示的指數有進行過偏移處理,所以指數的真實值e應為 ,因此可知除法操作的實際結果為 [文 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的平方根倒數,以之構造以y為自變量的函數,有 
將其代入牛頓法的通用公式 (其中 為首次近似值),
 ,其中  
整理有 ,對應的代碼即為y = y*(threehalfs - xhalf*y*y);

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

歷史與考究

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

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

不僅該演算法的原作者不明,人們也仍無法確定當初選擇這個「魔術數字」的方法。Chris Lomont曾做了個研究:他推算出了一個函數以討論此速演算法的誤差,並找出了使誤差最小的最佳R值0x5f37642f(與代碼中使用的0x5f3759df相當接近),但以之代入演算法計算並進行一次牛頓迭代後,所得近似值之精度仍略低於代入0x5f3759df的結果[文 17];因此Lomont將目標改為尋找在進行1-2次牛頓迭代後能得到最大精度的R值,在暴力搜尋後得出最佳R值為0x5f375a86,以此值代入演算法並進行牛頓迭代,所得的結果都比代入原始值(0x5f3759df)更精確[文 17],於是他的論文最後提到「如果可能我想詢問原作者,此速演算法是以數學推導還是以反覆試錯的方式求得?」[文 18]。在論文中,Lomont亦指出,64位元的IEEE754浮點數(即雙精度類型)所對應的魔術數字是0x5fe6ec85e7de30da,但後來的研究表明,代入0x5fe6eb50c7aa19f9的結果精確度更高(McEniry得出的結果則是0x5FE6EB50C7B537AA,精度介於兩者之間,Matthew Robertson給出的精度更高的值是0x5FE6EB50C7B537A9[6])。在Charles McEniry的論文中,他使用了一種類似Lomont但更複雜的方法來最佳化R值:他最開始使用窮舉搜尋,所得結果與Lomont相同[文 19];而後他嘗試用帶權二分法尋找最佳值,所得結果恰是代碼中所使用的魔術數字0x5f3759df,因此,McEniry認為,這一常數最初或許便是以「在可容忍誤差範圍內使用二分法」的方式求得[文 20]

註釋

  1. ^ 由於現代電腦系統對長整型的定義有所差異,使用長整型會降低此段代碼的可移植性。具體來說,由此段浮點轉換為長整型的定義可知,如若這段代碼正常執行,所在系統的長整型長度應為4位元組(32位元),否則重新轉為浮點數時可能會變成負數;而由於C99標準的廣泛應用,在現今多數64位元電腦系統(除使用LLP64數據模型的Windows外)中,長整型的長度都是8位元組[文 6][3]
  2. ^ 此處「浮點數」所指為標準化浮點數,也即有效數字部分必須滿足 ,可參見David Goldberg. What Every Computer Scientist Should Know About Floating-Point Arithmetic. ACM Computing Surveys. March 1991, 23 (1): 5–48. doi:10.1145/103162.103163. 
  3. ^ Lomont 2003確定R的方式則有所不同,他先將R分解為  ,其中  分別代表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]. (原始內容存檔於2009-02-09) (英語). 
  2. ^ quake3-1.32b/code/game/q_math.c. Quake III Arena. id Software. [2010-11-15]. (原始內容存檔於2011-02-17). 
  3. ^ Meyers, Randy. The New C: Integers in C99, Part 1. drdobbs.com. 2000-12-01 [2010-09-04]. (原始內容存檔於2012-05-27). 
  4. ^ Ruskin, Elan. Timing square root. Some Assembly Required. 2009-10-16 [2010-09-13]. (原始內容存檔於2010-08-25). 
  5. ^ Sommefeldt, Rys. Origin of Quake3's Fast InvSqrt() - Part Two. Beyond3D. 2006-12-19 [2008-04-19]. (原始內容存檔於2008-05-13). 
  6. ^ Matthew Robertson. A Brief History of InvSqrt (PDF). UNBSJ. 2012-04-24 [2023-11-23]. (原始內容存檔 (PDF)於2023-01-29). 

參考文獻

延伸閱讀

外部連結