概述
平方根倒數速算法,是用于快速計算1/Sqrt(x)的值的一種算法,在這里x需取符合IEEE 754標準格式的32位正浮點數。讓我們先來看這段代碼:
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 }
2010年,John Carmack公開了Quake III Arena的源代碼,這段代碼便出自其中,這種算法在游戲《雷神之錘III競技場》(Quake III Arena)被廣泛應用,作為《雷神之錘》3D游戲引擎的作者,該算法理所應當的被認為該是Carmark所創,但是后來Carmack在對Beyond3D上關于該段代碼的作者的討論回信表示這段代碼并不是出自他之手,也許是其開發小組的另一位成員所寫,而至今該算法的最終起源仍然是個謎。
魔數0x5f3759df
此算法最為牛叉之處在于注釋了what the fuck?的那一行代碼,此處將浮點數作為整數右移一位,通過一個神秘的十六進制數0x5f3759df進行減法運算,便完成了牛頓迭代需要的第一次猜測值,而這次猜測值已經很接近滿足精度的結果了,因此只通過了一次牛頓迭代就完成了所需精度的結果,其實就相當于直接進行計算就得到了所需結果。
32位浮點數在計算機中的存儲方式
如果要理解該段代碼除了魔數一行的代碼,就必須理解浮點數在計算機中的存儲方式。任何數據在計算機內存中都是以二進制形式存在的,浮點數也不例外, 32位的浮點數在計算機中的存儲方式如下圖,分為三個部分:
- 標志位(Sign),第31位為標志位,1代表負數,0代表正數;
- 指數部分(Exponent),指數部分共占8位,而 float類型規定其偏移量為127,由于指數可正可負,對于8位二進制數其表示范圍為[-128, 127];
- 尾數部分(Mantissa),表示有效數字位,由于尾數部分的整數部分恒為1,該位將不被存儲,因此原本23位的尾數部分可以表示的精度就變成了24位;
因此在二進制科學計數法中,我們可以將任意的32位浮點數表示為:
而平方根倒數速算法中的第9行的目的就是將浮點數轉化為32位整型數的表現形式。
牛頓迭代法
牛頓迭代法又稱為牛頓-拉菲森法(Newton-Raphson method),這種算法的原理就是一個求近似解的方法,該算法正是采用該方法來迭代平方根倒數的。
要求F(x)=0的解,首先選取一個接近函數F(x)零點的點(x0, F(x0)),過該點做切線求得其與X軸交點的x的值記為x1,該值通常會比x0更接近方程的解,然后不斷使用新的點進行迭代至滿足的精度為止。
在(x0, F(x0))點的斜率為,過該點的切線方程為,因此求得該切線與x軸的交點橫坐標值,因此簡化后的迭代公式為:
以求該平方根倒數求值為例,通過該方法轉化為求方程的解,申明,根據牛頓法,第一次迭代結果為:
而這也正好是算法中的第12行代碼對該算法的應用。
Lomont的逆向數學推理
對于該算法最讓人難以理解的就是魔數0x5f3759df,對于這個問題,Purdue大學的數學家Chris Lomont在論壇上得知后覺得非常有趣,于是決定研究一下它的工作原理。
Lomont在假設該算法成立的前提下(事實上已被廣泛證實該算法的正確性),希望通過數學推理推導出該值。下面簡要介紹一下Lomont的推導過程:
由于是求平方根倒數,因此假設所有浮點數的標志位均為0,浮點數二進制科學計數公式簡化為:
我們假設所求的魔數為R,那么第一次猜測值就可以表示為,其中i表示浮點數x的整數形式,R為整型,如下圖:
在對i進行按位右移的過程中,因為i的二進制指數位可能會被右移到尾數部分,所以必須按兩種情況分類討論:
1. 當i的指數部分為偶數,那么指數部分的最低位為0,指數位將不會被右移到尾數部分中,而真正的指數e = E - 127就是奇數,令e = 2d + 1,那么在經過運算后,y0的指數部分的值為:
由于我們所求的平方根倒數大于0,所以新的指數部分也必須大于0,且E的取值在[0, 254]之間,所以R1 ≥ 128;因為E是偶數,所以指數位并沒有移動到尾數中,所以運算之后尾數y0的尾數部分為:,
如果,那么初始猜測值為:
如果,二進制減法正如十進制中的減法運算,需向指數部分借位,此時,
定義
那么
因為e = 2d + 1,那么我們要求解的平方根倒數的近似值為:
所以y和y0的相對誤差則為:
那么根據要求相對誤差需小于0.125,則可以推導出R2的取值范圍(189.2, 190.7),所以R2=190=0xbe;
2. 當i的指數部分為奇數時,同理推導出:
如果我們希望以上兩種情況的相對誤差均小于0.125, R1確定且與E的奇偶無關,那么R1=190=0xbe,此時重新定義以上兩個誤差函數:
最終通過當M=0,1,以及R2=M/2的幾個臨界點獲取相對誤差最大值函數,然后在所有函數中獲取使得這些誤差函數值都最小的R2的值,這個值便是我們要找的結果,分類討論一共得到了9個關于R2(取值范圍是[0, 1))的函數:
最終這個求值過程交給Mathematica來完成,也不知為何我最后求得的結果和Lomont略有差異:
r0 = 0.432744889959443195468521587014
我求出的結果:
r0 = 0.432744834277619894180588744347
其他討論
Lomont求解魔數0x5f3759df的推導過程的前提是假設算法中
i = 0x5f3759df - ( i >> 1 );
這行代碼成立,通過反推來求解魔數的,但是這行代碼最初是如何得出的,依據是什么,Lomont并沒有給出更多的解釋或猜測,好在其他的論壇也有相關的討論。
在一則slashdot.org關于Origin of Quake3's Fast InvSqrt()的討論中,kent.dickey的回復令人印象深刻,而我個人也非常贊同他的思路,非常簡單的推導,也許他的這種思路才是該算法的最初雛形。
他在回復中說:對于浮點數平方根倒數的求解,指數部分的初始猜測值就是對指數部分減求反(暫時忽略尾數部分,因為對于2以內的求解,一步就可以得到結果),比如求16的平方根倒數:
但是由于在內存中浮點數特殊的存儲方式,因此在內存中指數的整型值為:
NewExpIntValueInMemory=(ActualExp+127) << 23
我們想要的指數結果是:
NewActualExp=-ActualExp/2
由于在內存中:Exp=ActualExp+127,那么ActualExp=Exp-127,所以:
NewExp-127=-(Exp-127)/2
那么:
NewExp=127-(Exp-127)/2=(127×3)/2-Exp/2
那 么指數在內存中指數的整型值為:
所以在完全忽略尾數部分的前提下,初始猜測近似值為:
i=0x5F400000-i≫1
隨后kent.dickey試圖使用類似方法找到對于尾數部分的類似特定模式,遺憾的是沒能找到,也許作者當初在有了這個思路(如果該思路真的就是原算法的雛形)之后,也采用了類似Lomont的暴力求解來獲得更精確的初始猜測。
參考文獻
- 一個Sqrt函數引發的血案http://www.cnblogs.com/pkuoliver/archive/2010/10/06/1844725.html
- 平方根倒數速算法http://zh.wikipedia.org/wiki/%E5%B9%B3%E6%96%B9%E6%A0%B9%E5%80%92%E6%95%B0%E9%80%9F%E7%AE%97%E6%B3%95
- FAST INVERSE SQUARE ROOT http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf
- Quake 3 1.32 Source Code http://www.shacknews.com/file/7443/quake-3-132-source-code
- Origin of Quake3's Fast InvSqrt() http://www.beyond3d.com/content/articles/8/
- Origin of Quake3's Fast InvSqrt() http://games.slashdot.org/story/06/12/01/184205/origin-of-quake3s-fast-invsqrt
- 浮點數在計算機中存儲方式http://www.cnblogs.com/jillzhang/archive/2007/06/24/793901.html
- 浮點數的表示與類型轉換http://www.cnblogs.com/yaozhongxiao/archive/2009/09/24/1573203.html
文章列表
留言列表