平方根倒數速演算法

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

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

此演算法最早被認為是由約翰・卡馬克於 90 年代前期在的開發中使用，後來則於 1999 年在《雷神之錘 III 競技場》的原始碼中應用，但直到 2002－2003 年間才在 Usenet 一類的公共論壇上出現。後來的調查顯示，該演算法在這之前就於計算機圖形學的硬體與軟體領域有所應用，如 SGI 和 3dfx 就曾在產品中應用此演算法，但至今為止仍未能確切知曉演算法中所使用的特殊常數的起源。

演算法的切入點

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

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

可求得向量 v 的歐幾里得範數，此演算法正類如對歐幾里得空間的兩點求取其歐幾里得距離，

而求得的就是標準化的向量，若以代表，則有，

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

為了加速影像處理單元計算，《雷神之錘 III 競技場》使用了平方根倒數速演算法，而後來採用現場可程式設計邏輯閘陣列的頂點著色器也應用了此演算法。

程式碼概覽

下列程式碼是《雷神之錘 III 競技場》原始碼中平方根倒數速演算法之例項。示例去除了 C 預處理器的指令，但附上了原有的註釋（括號內為註釋的翻譯）：

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 年代初（也即該演算法發明的大概時間），軟體開發時通用的多是從查詢表中取得近似值，而這段程式碼取近似值耗時比之更短，達到精確度要求的速度也比通常使用的浮點除法計算法快四倍，雖然此演算法會損失一些精度，但效能上的巨大優勢已足以補償損失的精度。由程式碼中對原資料的變數型別宣告為 float 可看出，這一演算法是針對 IEEE 754 標準格式的 32 位浮點數設計的，不過據 Chris Lomont 和後來的 Charles McEniry 的研究看，這一演算法亦可套用於其他型別的浮點數上。

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

2.1 將浮點數轉化為整數

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

如上所述，一個有符號正整數在二進位制補碼系統中的表示中首位為 0，而後面的各位則用於表示其數值。將浮點數取儲存為整數時，該整數的數值即為，其中 E 表示指數，M 表示有效數字；若以上圖為例，圖中樣例若作為浮點數看待有，，則易知其轉化而得的整數型號數值為。由於平方根倒數函式僅能處理正數，因此浮點數的符號位（即如上的 Si）必為 0，而這就保證了轉換所得的有符號整數也必為正數。以上轉換就為後面的計算帶來了可行性，之後的第一步操作（邏輯右移一位）即是使該數的長整形式被 2 所除。

2.2 “魔術數字”

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

，其中

再繼續看 McEniry 2007 裡的進一步說明：

對等式的兩邊取二進位制對數（，即函式的反函式），有

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

參照首段等式代入，，與，有：

移項整理得：

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

，其中.

最後匯出的等式即與上節程式碼中一行相契合，由此可見，在平方根倒數速演算法中，對浮點數進行一次移位操作與整數減法，就可以可靠地輸出一個浮點數的對應近似值。到此為止，McEniry 只證明了，在常數 R 的輔助下，可近似求取浮點數的平方根倒數，但仍未能確定程式碼中的 R 值的選取方法。

關於作一次移位與減法操作以使浮點數的指數被 - 2 除的原理，Chris Lomont 的論文中亦有個相對簡單的解釋：以為例，將其指數除 - 2 可得；而由於浮點表示的指數有進行過偏移處理，所以指數的真實值 e 應為，因此可知除法操作的實際結果為，這時用 R（在此即為 “魔術數字” 0x5f3759df）減之即可使指數的最低有效數位轉入有效數字域，之後重新轉換為浮點數時，就能得到相當精確的平方根倒數近似值。在這裡對常數 R 的選取亦有所講究，若能選取一個好的 R 值，便可減少對指數進行除法與對有效數字域進行移位時可能產生的錯誤。基於這一標準，0xbe 即是最合適的 R 值，而 0xbe 右移一位即可得到 0x5f，這恰是魔術數字 R 的第一個位元組。

2.3 精確度

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

2.4 牛頓法提高精度

在進行了如上的整數操作之後，示例程式再度將被轉為長整型的浮點數迴轉為浮點數（對應），並對其進行一次浮點運算操作（對應），這裡的浮點運算操作就是對其進行一次牛頓法迭代，若以此例說明：

所求的是 y 的平方根倒數，以之構造以 y 為自變數的函式，有，

將其代入牛頓法的通用公式（其中為首次近似值），

有，其中，。

整理有，對應的程式碼即為。

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

歷史與考究

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

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