contributed by < willy-liu
>
Assume x is a single precision floating point number and always >= 1. Calculate square root (casting to int for easing complexity) without FPU, as precise as FPU operations.
首先注意到為了讓題目可以在十分鐘內回答出來,老師簡化了回傳值,並且限定輸入>=1,而我當下太在意小數點的精確度,而回答了 IEEE 754 的浮點數規格,然後打算用 LUT 的方式確保小數點,但這題若只要回傳"整數",我們就需要確認精確度的問題。
先考慮計算無條件捨去的情況
在無法使用 FPU 的情況,我只能使用int
和位元運算來實現,最基本的方式是透過二分搜尋法來逼近。
p.s. 原本條件為mid * mid <= num
,避免溢位改成mid <= num / mid
因為是二分搜尋法,所以迴圈要跑次,執行時間會隨著值上升變久,而且中間需要用到除法(如果要小數後一位四捨五入可以在最後判斷是否
if ((ans*10+5) * (ans*10+5) <= num * 100) ans++;
)
考慮更快的方法,直式開方法(又叫做digit-by-digit法),計算平方根時我們就是要找到一個最大的使,假設a是b的前一位,我們可以將視為,改寫式子為,至此,就可以從高位數往低位數去逼近平方根。
10進位範例:
x = 6242 =
由於y的每位數會貢獻x的2位數,所以一次要考慮兩位數。
首先,找到最大的a使,得出a = 7
故x = 6242 = =
首兩位"62"由於a的貢獻只會剩下
接著再往後考慮兩位,1342會是的貢獻,得出b=9
1342經過b的貢獻會剩下1342-
根據這個公式可以再無限往下擴展,例如c為b的後一位數
一樣再往下取兩位數100,會是 的貢獻,得出c=0
…
,
其中
是前 位組合成的數字(不含小數點,視為整數)
是當前被減數
是本輪要決定的新數字
套用到二進制,每一輪就只要決定新數字要是0/1就好
這個方法僅使用位元運算和加減法,而且無論輸入是多少都只要進行16次的迴圈,如果要考慮小數後一位,一樣可以在最後透過位移,再多算一次小數後一位的情況,如果小數後一位的位元是"1",代表小數部分 0.5,所以回傳的int
就要加一。
這段程式碼其實也等價於res += (num >= res + 1 ? 1 : 0);
,因為僅考慮下一位帶來的影響,做位移僅僅是為了好讀。
在x較小時誤差會比較多,最大高達40~50%。
由於要讓回傳值也是float,首先要了解IEEE 754中如何定義一個single precision floating point
由定義可以知道一個 IEEE 754 的單精度浮點數的值表示法如下
在完成「四捨五入到小數點第一位」的處理後,我想到可以把這個方法向更高精度推進,並且將小數部分也一併納入計算,也就是說,只要針對有效數字(即 1.23)計算開方,然後再將結果乘以對應的指數位移,就能得出最終值。若將這個思路應用到浮點運算中,只需對 fraction 部分進行開方計算,計算完成後再把 exponent 按原先偏移量調整即可。
這邊要注意的是 exponent 要分為奇數和偶數考慮,奇數時要借位 mantissa (改進後使用位元運算避免分支),此時 mantissa 可能是24 或 25 bits(包含IEEE 754省略的1),為了提高計算的精度,將其<<7再進行直式開方(要記得把my_sqrtf_bitwise輸入、回傳改成unsigned int),才能有效利用完整的unsigned int
,經過直式開方的值會剩下16bit,再<<8變回 IEEE 754 的 fraction 格式,最後再進行float
的重組。
如果要再改善精度,完整利用float
的fraction
部分,我們可以考慮將直式開方的輸入、輸出改為long long
,然後從<<46的 bit 開始計算, mantissa 傳入前也先轉型並<<23,就可以完整利用 fraction 的23個 bits ,精確度也會提升到跟sqrtf
幾乎一致,但在測試後發現 mantissa 的最後一個 bit 可能差1。
為了找出原因,我查閱了sqrtf
的 man page,雖然其中沒有說明當數值無法完整表示時的處理方式,但可以確定它會遵循 IEEE 754 的規範。 rounding 的相關摘要如下:
4.3.1 Rounding-direction attributes to nearest
In the following two rounding-direction attributes, an infinitely precise result with magnitude at least shall round to ±∞ with no change in sign; here emax and p are determined by the destination format (see 3.3). With:
- roundTiesToEven, The floating-point number nearest to the infinitely > precise result shall be delivered. If the two nearest floating-point numbers bracketing an unrepresentable infinitely precise result are equally near, the one with an even least-significant digit shall be delivered. If that is not possible, the one larger in magnitude shall be delivered. (This can happen for one-digit precision—for example, when rounding 9.5 to one digit both 9 and
1×10^1
have odd significands.)- roundTiesToAway, The floating-point number nearest to the infinitely precise result shall be delivered. If the two nearest floating-point numbers bracketing an unrepresentable infinitely precise result are equally near, the one with larger magnitude shall be delivered.
很顯然,在sqrtf
的情況需要再額外考慮下一位的值進行 rounding ,因此,在加入res += (num >= res + 1 ? 1 : 0);
後得到的結果就與sqrtf完全一致。
牛頓法的概念就是透過微分找到切線的值,然後用新的繼續迭代去逼近這個可微函數的解。
可以從函數在當前點處的一階泰勒展開(切線近似)出發:
令上述切線在 (y=0) 處求交點:
另外,從上圖可以發現的選擇很重要,如果距離解太遠的話會需要迭代很多次,如果一開始就離解很近,也許只需要迭代1~2次就可以與解的誤差很小,通常會使用或是出發。
由於我要計算的是,實際上要找的就是的解,帶入公式後就變成
接著我們來思考如何去找到一個更好的,由前面知道float的表示法為(簡化 mantissa 部分以及省略 sign bit)
mantissa 的部分會很接近,而對於指數部分我們取近似
在 IEEE-754 二進制浮點裡,加上 63 的偏移量就對應到常數位元
然而在測試後注意到由於我們同時會對尾數做右移 的操作,實際上那個「.5」加到 exponent 上之後,不會和原本的 mantissa bits 重疊覆蓋
誤差分析( magic number 使用0x1fc00000
):
在0.01~100取1000個點測試,每個點測試1000次。
可以看到加了初始值的預估後只做一次牛頓法就可以非常接近正確值,甚至不使用牛頓法,單純預估值的平均誤差大約在1.6%,最大誤差僅6%左右(Max Error = 6.044607%, Mean Error = 1.555732%
),在一些場景下這樣的精確度也許就已經足夠。
相較之下若將的話做了超過四次迭代,平均誤差值仍然高達2%以上,比計算的預估值還差,由此可知這樣的分析可以大幅降低計算量。
然而,看完使用的時間後我發現,雖然預估初始值可以大幅降低牛頓法的迭代次數,但是花費的時間卻比較長,就算 magic sqrt 只做一次牛頓法,速度還是略慢於 plain sqrt 做10次,且可以達到一樣的精確度,就算用了union也是相同的結果,猜測原因是數值在記憶體複製的花費時間可能比想像中的長。
和Linux 核心的 PELT的關係,看過原始碼後發現它使用定點數計算EWMA。
所以可以將原本用浮點數迭代的部分改用Q16.16定點數進行運算,並且換算時也要手動提取 IEEE 754 的 float field 進行轉換(參考quiz5)。
輔助函式:
以下是實作程式碼:
由於選擇的是 unsigned Q16.16 定點數,所以整數部分可以表示的範圍只有 而小數部分的精確度只能表示到。
誤差分析:
由於前面提過,經過計算的預估值已經非常準確,所以誤差都很小,然而最大誤差卻和Q16.16的精確度差了一個數量級,
分析原因:在 Q16.16 定點格式中,最小可表達增量為,若當前迭代誤差已接近此量化步長時,下一次更新將遠小於,因此在定點運算時會被捨入為,無法再繼續縮小。
令定點最小量化步長為,每次牛頓法迭代中,主要有兩次定點除法與一次定點乘法會各自捨入:
因此,每迭代一次,最多會產生約的累積捨入誤差。若迭代次,則量化誤差上界為,帶入
再加上最後一次從定點轉回浮點時,尾數欄位也會捨入一個,\
與實測的誤差量級相符(因精確誤差會累積,故略大於理論上界)。