--- tags: sysprog2018 --- # 2018q1 Homework2 (assessment) contributed by <`HexRabbit`> ## 第三周測驗題 延續 [從 $\sqrt{2}$ 的運算談浮點數](https://hackmd.io/s/ryOLwrVtz),假設 double 為 64-bit 寬度,考慮以下符合 IEEE 754 規範的平方根程式,請補上對應數值,使其得以運作: ```clike #include <stdint.h> /* A union allowing us to convert between a double and two 32-bit integers. * Little-endian representation */ typedef union { double value; struct { uint32_t lsw; uint32_t msw; } parts; } ieee_double_shape_type; /* Set a double from two 32 bit ints. */ #define INSERT_WORDS(d, ix0, ix1) \ do { \ ieee_double_shape_type iw_u = { \ .parts.msw = ix0, \ .parts.lsw = ix1, \ }; \ (d) = iw_u.value; \ } while (0) /* Get two 32 bit ints from a double. */ #define EXTRACT_WORDS(ix0, ix1, d) \ do { \ ieee_double_shape_type ew_u; \ ew_u.value = (d); \ (ix0) = ew_u.parts.msw; \ (ix1) = ew_u.parts.lsw; \ } while (0) static const double one = 1.0, tiny = 1.0e-300; double ieee754_sqrt(double x) { double z; int32_t sign = 0x80000000; uint32_t r, t1, s1, ix1, q1; int32_t ix0, s0, q, m, t, i; EXTRACT_WORDS(ix0, ix1, x); /* take care of INF and NaN */ if ((ix0 & KK1) == KK2) { /* sqrt(NaN) = NaN, sqrt(+INF) = +INF, sqrt(-INF) = sNaN */ return x * x + x; } /* take care of zero */ if (ix0 <= 0) { if (((ix0 & (~sign)) | ix1) == 0) return x; /* sqrt(+-0) = +-0 */ if (ix0 < 0) return (x - x) / (x - x); /* sqrt(-ve) = sNaN */ } /* normalize x */ m = (ix0 >> 20); if (m == 0) { /* subnormal x */ while (ix0 == 0) { m -= 21; ix0 |= (ix1 >> 11); ix1 <<= 21; } for (i = 0; (ix0 & 0x00100000) == 0; i++) ix0 <<= 1; m -= i - 1; ix0 |= (ix1 >> (32 - i)); ix1 <<= i; } m -= KK3; /* unbias exponent */ ix0 = (ix0 & 0x000fffff) | 0x00100000; if (m & 1) { /* odd m, double x to make it even */ ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; } m >>= 1; /* m = [m/2] */ /* generate sqrt(x) bit by bit */ ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; q = q1 = s0 = s1 = 0; /* [q,q1] = sqrt(x) */ r = 0x00200000; /* r = moving bit from right to left */ while (r != 0) { t = s0 + r; if (t <= ix0) { s0 = t + r; ix0 -= t; q += r; } ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; r >>= 1; } r = sign; while (r != 0) { t1 = s1 + r; t = s0; if ((t < ix0) || ((t == ix0) && (t1 <= ix1))) { s1 = t1 + r; if (((t1 & sign) == sign) && (s1 & sign) == 0) s0 += 1; ix0 -= t; if (ix1 < t1) ix0 -= 1; ix1 -= t1; q1 += r; } ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; r >>= 1; } /* use floating add to find out rounding direction */ if ((ix0 | ix1) != 0) { z = one - tiny; /* trigger inexact flag */ if (z >= one) { z = one + tiny; if (q1 == (uint32_t) 0xffffffff) { q1 = 0; q += 1; } else if (z > one) { if (q1 == (uint32_t) KK4) q += 1; q1 += 2; } else q1 += (q1 & 1); } } ix0 = (q >> 1) + 0x3fe00000; ix1 = q1 >> 1; if ((q & 1) == 1) ix1 |= sign; ix0 += (m << KK5); INSERT_WORDS(z, ix0, ix1); return z; } ``` ## 想法 & 思考 > 為避免畫面紊亂,部分解說直接記錄於註解中 > [color=orange] **64-bit double presition float data fromat** | Sign | Exponent | Fraction | | --- | --- | --- | | 1 | 11 | 52 | $Exponent > 0: Sign * 2^{Exponent-1023} * 1.Fraction$ $Exponent == 0: Sign * 2^{-1022} * 0.Fraction$ 題目落落長,總之先瞭解各個function與macro在做些什麼吧 - `ieee_double_shape_type`: `union` 用來簡化操作double - `INSERT_WORDS(d, ix0, ix1)`: `macro` 將兩個32bit的int ix0,ix1放入一個double d 中,ix0在高位 ix1在低位 (只適用於little-endian的機器上) - `EXTRACT_WORDS(ix0, ix1, d)`: `macro` 功能與INSERT_WORDS相反 - `ieee754_sqrt(x)`: `function` return x 的平方根 瞭解這些後就可以開始解題了,題目首先把 x 提取至 ix0, ix1中,再來出現了第一個問題 ### Handle INF & NaN ```clike /* take care of INF and NaN */ if ((ix0 & KK1) == KK2) { /* sqrt(NaN) = NaN, sqrt(+INF) = +INF, sqrt(-INF) = sNaN */ return x * x + x; } ``` 首先觀察到了一個奇怪的詞`sNaN`,查詢維基百科後發現NaN透過判斷Fraction的第一位是否為零,可以分為"signal NaN" 與 "quiet NaN"兩種,差異在於前者在FPU運算時會拋出 Exception (Linux下是 [SIGFPE](http://pubs.opengroup.org/onlinepubs/009696699/basedefs/signal.h.html)) 而後者不會。 另外也發現對於浮點數 X 的任何有關 NaN 的數學判斷式,只有 != 會成立 | Comparison | NaN >= X | NaN <= X | NaN > X | NaN < X | NaN == X | NaN != X | --- | --- | --- | --- | --- | --- | --- | Result | Always False | Always False | Always False | Always False | Always False | Always True | 不過只要瞭解這段式子是用來處理 INF 和 NaN, 就可以知道 KK1 的作用為 mask Exponant 部分以判斷 INF 和 NaN, 而 INF 和 NaN 在 Exponent 中的 bit 皆為 1,也就知道 KK2 了。 ::: success KK1 = 0x7FF00000 KK2 = 0x7FF00000 ::: ### Handle zero and negative 程式的下一部份是處理 x 為零(其實還有負數)的情形,稍微將其簡化 ```clike /* take care of zero and negative */ if (ix0 <= 0) { if (x == 0) return x; /* sqrt(+-0) = +-0 */ if (ix0 < 0) return (x - x) / (x - x); /* sqrt(-ve) = sNaN */ } ``` :::warning 不太瞭解 (x - x) / (x - x) 的意思,這跟 0 / 0 差在哪? ::: > 提示: x-x 真的 = 0 嗎? > [name=ryanpatiency][color=green] > 像是 (-1.0 - -1.0) 這應該是 0 吧 > [name=HexRabbit][color=orange] :::info 新發現,0 / 0 會觸發 FPU 的 exception,而 0.0 / 0.0 不會 ::: 查閱 [IEEE 754 浮點數標準](http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=4610935) 7.2 節 Invalid operation 部分, :::info **For operations producing results in floating-point format, the default result of an operation that signals the invalid operation exception shall be a quiet NaN** that should provide some diagnostic information (see 6.2). These operations are: - a) any general-computational or signaling-computational operation on a signaling NaN (see 6.2), except for some conversions (see 5.12) - b) multiplication: multiplication(0, ∞) or multiplication(∞, 0) - c) fusedMultiplyAdd: fusedMultiplyAdd(0, ∞, c) or fusedMultiplyAdd(∞, 0, c) unless c is a quiet NaN; if c is a quiet NaN then it is implementation defined whether the invalid operation exception is signaled - d) addition or subtraction or fusedMultiplyAdd: magnitude subtraction of infinities, such as: addition(+∞, −∞) - e) **division: division(0, 0)** or division(∞, ∞) - f) remainder: remainder(x, y), when y is zero or x is infinite and neither is NaN - g) squareRoot if the operand is less than zero - h) quantize when the result does not fit in the destination format or when one operand is finite and the other is infinite ::: 發現若一個運算的結果是以浮點數格式呈現, 則運算時遇到 invalid operation 時將不會觸發 Exception ( 即為 quiet NaN ) ### Fraction normalization 再來是對 Fraction 部分做 normalize, 將形式全部改為`1.XXXXXXXXXX`,並提取出正確的 Exponent 做 sqrt ```clike /* normalize x */ m = (ix0 >> 20); /* 擷取Exponent部分 */ if (m == 0) { /* subnormal x */ /* 移動Fraction直到指數位調整為1 -> 1.fraction (已經失去指數的意義,只為了後續計算使用)*/ /* 先調整到ix0不為零,使用21是因為若ix1的MSB是1的話就不用繼續操作了 */ while (ix0 == 0) { m -= 21; /* 把乘上的次方數從m中扣除 */ ix0 |= (ix1 >> 11); ix1 <<= 21; } /* 繼續調整到指數位為1 */ for (i = 0; (ix0 & 0x00100000) == 0; i++) ix0 <<= 1; m -= i - 1; ix0 |= (ix1 >> (32 - i)); ix1 <<= i; } m -= KK3; /* unbias exponent */ ix0 = (ix0 & 0x000fffff) | 0x00100000; /* 去除ix0的 Exponent 其餘部分,只留下1 */ if (m & 1) { /* 將奇數指數位乘入 Fraction 部分 */ ix0 += ix0 + ((ix1 & sign) >> 31); /* 可能進位 */ ix1 += ix1; } m >>= 1; /* 指數位sqrt m = [m/2] */ ``` ::: success KK3 = 1023 ::: :::info 常常出現的程式碼片段,會將 Fraction 部分左移一位 ``` ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; ``` ::: ### Digit-by-digit square root method ```clike /* generate sqrt(x) bit by bit */ /* 左移一位,若是除不盡最後可以多出一位拿來做捨入 */ ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; q = q1 = s0 = s1 = 0; /* [q,q1] = sqrt(x) */ r = 0x00200000; /* r = moving bit from right to left */ while (r != 0) { t = s0 + r; /* 實際上要考慮 r*(s0 + r) <= ix0 對於所有 r,但是因為 r 不是 0 就是 1, * 所以先考慮 r=1 的情況,再以 t <= ix0 判斷是否繼續操作 */ if (t <= ix0) { s0 += 2*r; /* 以更易讀的做法取代 s0 = t + r; */ ix0 -= t; q += r; } /* Fraction 部分向左位移,r向右位移,可以讓產生出來的q保持連續 */ ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; r >>= 1; } r = sign; while (r != 0) { t1 = s1 + r; t = s0; if ((t < ix0) || ((t == ix0) && (t1 <= ix1))) { s1 = t1 + r; if (((t1 & sign) == sign) && (s1 & sign) == 0) s0 += 1; ix0 -= t; if (ix1 < t1) ix0 -= 1; ix1 -= t1; q1 += r; } ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; r >>= 1; } ``` 本段是對小數部分 ( 1.Fraction ) 做 sqrt, 我自己看了幾個小時其實看不太出甚麼端倪,直接代數字進去做也是霧煞煞 後來在網路上找到一個[影片](https://www.youtube.com/watch?v=G_4HE3ek4c4),才發現原來有這樣一種奇技淫巧可以對應這裡的實作 再翻閱 wiki 上 [Digit-by-digit calculation](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation) 的部分,有提到此作法的原理和證明, 試著套用到二進位上: :::info 假設 $Z$ 是一個有兩個位數的二進位數 $'XY'$,則 $Z^{2} = (2*X+Y)^{2} = 4*X^{2} + 2*2*XY + Y^{2}$ 所以若是可以找到一個 $X$ 使 $4*X^{2}$ 小於等於 $Z^{2}$,也就是$X^{2}<=Z^{2}$的前兩位,即為找到$X$ (因為乘以 4 與左移 2 位相等) 接著從 $Z$ 中扣除 $4*X^{2}$ 後得到 $Z2$, 接著只要想辦法找出一個 $Y$ 滿足 $Y(2*2*X+Y) = Z2$ 即可 這樣便可以繼續推廣到 N 位 ::: 若以圖像表示程式碼中變數和算式的關係的話 ``` >>>>>>> _ ------------------------ _ | 01 .10 11 10 01 ``` ``` >>>>>>> 1 _ ------------------------ 1 | 01 .10 11 10 01 (1*2 = 10) - 1 ------ 10_ | 10 ``` ``` >>>>>>> 1 0 _ ------------------------ 1 | 01 .10 11 10 01 - 1 ------ 100 | 10 (0*2 = 0) - 0 ------ 100_ |10 11 ``` ``` >>>>>>> 1 0 1 X _ <=== q, q1 ------------------------ 1 | 01 .10 11 10 01 - 1 ------ r = any single digit in q 100 | 10 - 0 ------ 1001 |10 11 (1*2 = 10) - 10 01 ------ t ===> 1010X | 10 10 (X*2 = AB) - (X*1010X) (10100+AB = ABCDE) ------ ^ ABCDE_ | ..... | s0 ``` :::warning 本段開始的這個左位移在我看來是不必要的, ``` ix0 += ix0 + ((ix1 & sign) >> 31); ix1 += ix1; r = 0x00200000; ``` 可以將 r 的值修改為 0x00100000 並和後一段中出現的 >> 1 一併移除 ``` ix0 = (q >> 1) + 0x3fe00000; ix1 = q1 >> 1; ``` 可是結果會出現微小誤差($10^{-14}$),仍待釐清 ::: :::success => 為了多計算一位,做完 rounding 後能夠更加逼近正確值 ::: ### Rounding 再來是做捨入的部分,在上一段的最初將需要計算的部分多左移了一位, 所以會多出一位可以做捨入,讓數值更加精確。 ``` /* use floating add to find out rounding direction */ /* 不為零代表不是完全平方數 */ if ((ix0 | ix1) != 0) { z = one - tiny; /* trigger inexact flag */ if (z >= one) { z = one + tiny; /* 全為1的情況要進位(最後一位四捨五入) */ if (q1 == (uint32_t) 0xffffffff) { q1 = 0; q += 1; } else if (z > one) { if (q1 == (uint32_t) KK4) q += 1; q1 += 2; } else q1 += (q1 & 1); } } ``` :::warning 實在不是很瞭解這是在做甚麼,還要去查查 ```clike z = one - tiny; /* trigger inexact flag */ if (z >= one) ``` ::: ### Finalize 最後將 ix0 加上 bias 並且把 Exponent 和 Fraction 放回來,再跟 ix1 組裝即完成作業。 ```clike /* 右移丟棄 Fraction 最後一位 */ ix0 = (q >> 1) + 0x3fe00000; /* +1022+1 (q 本身在指數位有 1) */ ix1 = q1 >> 1; if ((q & 1) == 1) ix1 |= sign; ix0 += (m << KK5); INSERT_WORDS(z, ix0, ix1); ``` :::success KK5 = 20 :::