# 2020q3 Homework5 (quiz5) contributed by < `Rsysz` >. ###### tags: `sysprog` >[測驗題](https://hackmd.io/@sysprog/2020-quiz5) ## 1. ```cpp #include <stdio.h> #include <stdlib.h> double divop(double orig, int slots) { if (slots == 1 || orig == 0) return orig; int od = slots & 1; double result = divop(orig / D1, od ? (slots + D2) >> 1 : slots >> 1); if (od) result += divop(result, slots); return result; } ``` 將浮點數 `orig` 除以大於 `0` 的整數 `slots` 若 `slots` 為偶數 $$ \frac{orig/2}{slots/2}=\frac{orig}{slots} $$ 若 `slots`為奇數,透過 `result += divop(result, slots)` 補償 $$ \frac{orig/2}{(slots+1)/2}+\frac{\frac{orig/2}{(slots+1)/2}}{slots}=\frac{orig/2}{(slots+1)/2}\times(1+\frac{1}{slots})=\frac{orig/2}{(slots+1)/2}\times\frac{(slots+1)}{slots}=\frac{orig}{slots} $$ 對奇數 `slots` 加 `1` 後右移與補償,巧妙地加速計算速度,D1 = `(c) 2`, D2 = `(d) 1` ## 2. ```cpp #include <stdint.h> /* A union allowing us to convert between a float and a 32-bit integers.*/ typedef union { float value; uint32_t v_int; } ieee_float_shape_type; /* Set a float from a 32 bit int. */ #define INSERT_WORDS(d, ix0) \ do { \ ieee_float_shape_type iw_u; \ iw_u.v_int = (ix0); \ (d) = iw_u.value; \ } while (0) /* Get a 32 bit int from a float. */ #define EXTRACT_WORDS(ix0, d) \ do { \ ieee_float_shape_type ew_u; \ ew_u.value = (d); \ (ix0) = ew_u.v_int; \ } while (0) static const float one = 1.0, tiny = 1.0e-30; float ieee754_sqrt(float x) { float z; int32_t sign = 0x80000000; uint32_t r; int32_t ix0, s0, q, m, t, i; EXTRACT_WORDS(ix0, x); /* take care of zero */ if (ix0 <= 0) { if ((ix0 & (~sign)) == 0) return x; /* sqrt(+-0) = +-0 */ if (ix0 < 0) return (x - x) / (x - x); /* sqrt(-ve) = sNaN */ } /* take care of +INF and NaN */ if ((ix0 & 0x7f800000) == 0x7f800000) { /* sqrt(NaN) = NaN, sqrt(+INF) = +INF,*/ return x; } /* normalize x */ m = (ix0 >> 23); if (m == 0) { /* subnormal x */ for (i = 0; (ix0 & 0x00800000) == 0; i++) ix0 <<= 1; m -= i - 1; } m -= 127; /* unbias exponent */ ix0 = (ix0 & 0x007fffff) | 0x00800000; if (m & 1) { /* odd m, double x to make it even */ ix0 += ix0; } m >>= 1; /* m = [m/2] */ /* generate sqrt(x) bit by bit */ ix0 += ix0; q = s0 = 0; /* [q] = sqrt(x) */ r = QQ0; /* 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; r >>= 1; } /* use floating add to find out rounding direction */ if (ix0 != 0) { z = one - tiny; /* trigger inexact flag */ if (z >= one) { z = one + tiny; if (z > one) q += 2; else q += (q & 1); } } ix0 = (q >> 1) + QQ1; ix0 += (m << QQ2); INSERT_WORDS(z, ix0); return z; } ``` 首先看到 `do while(0)` 封裝的 `marco`,這種用法是為了確保替換後程式正確編譯與減少內部 `Bug` 的發生,再來看到各 `marco` 的功能。 * ` INSERT_WORDS ` 將 `int ix0` 轉為 `float d` * ` EXTRACT_WORDS ` 將 `float d` 轉為 `int ix0` * 因無論 `unsigned` 與否,內部數值皆為相同的,因此皆以 `int` 表示 接著看到整體程式碼,首先將 `float x` 轉為 `int ix0` * `ix0 <= 0` 也就是 `float x` 為負數時有兩個結果 * `(ix0 & (~sign)) == 0` 表示除 `sign` 以外皆是 `0`,代表 `float x`為 `+-0` 開根號還是 `0`,回傳 `x` * 先前條件不滿足且 `ix0 < 0`,表示 `float x` 為負浮點數,無法開根號,因此回傳 `NaN` ```cpp /* take care of zero */ if (ix0 <= 0) { if ((ix0 & (~sign)) == 0) return x; /* sqrt(+-0) = +-0 */ if (ix0 < 0) return (x - x) / (x - x); /* sqrt(-ve) = sNaN */ } ``` 接著處理 `INF` 與 `NaN`,先前條件不滿足且 `(ix0 & 0x7f800000) == 0x7f800000` 表示 `float x` 的 `Exponent` 部分皆為 `1` * `+INF` 表示為 $1_{sign}11111111_{exponent}..._{fraction}$ `fraction` 皆為 `0` * `NaN` 表示為 $0_{sign}11111111_{exponent}..._{fraction}$ `fraction` 部分皆可,此外先前 `(ix0 < 0)` 已排除 `sign` 為 `1` 的結果 ```cpp /* take care of +INF and NaN */ if ((ix0 & 0x7f800000) == 0x7f800000) { /* sqrt(NaN) = NaN, sqrt(+INF) = +INF,*/ return x; } ``` ![](https://i.imgur.com/qNgOu3S.png) * 對浮點數做正規化,首先取出 `ix0` 的前九個bit 也就是 `float x` 的 `sign` 與 `exponent` 部分 * 若 `m == 0` 則對 `ix0` 做左移,直到 `exponent` 部分的 $2^0$ 為 `1` * 因 `m` 表示 `exponent` 部分,根據上圖對浮點數的正規化,`m = m + 1 - i` * 接著將 `m - 127` 得到對應 `exponent` 的數字,並以 `ix0 = (ix0 & 0x007fffff) | 0x00800000` 擷取 `ix0` 的後 `23` bit並將第 `24` bit 設為 `1`,得到$1.fraction$ * 最終得到$2^{m} \times ix0,1 \leq ix0 < 2$ 接著為了將$\sqrt{2^{m}} = 2^{\frac{m}{2}}$,若 `m` 為奇數,將多的 `2` 乘至 `ix0` 部分,再將 `m` 除以 `2` 得到$2^{\frac{m}{2}} \times (2 \times 1.fraction),1.fraction \leq ix0 \leq (2 \times 1.fraction)$ * 又 $1 \leq 1.fraction < 2,所以1 \leq ix0 < 4$,整理如下$$2^{\frac{m}{2}} \times ix0,1 \leq ix0 < 4,m \in even$$ ```cpp /* normalize x */ m = (ix0 >> 23); if (m == 0) { /* subnormal x */ for (i = 0; (ix0 & 0x00800000) == 0; i++) ix0 <<= 1; m -= i - 1; } m -= 127; /* unbias exponent */ ix0 = (ix0 & 0x007fffff) | 0x00800000; if (m & 1) { /* odd m, double x to make it even */ ix0 += ix0; } m >>= 1; /* m = [m/2] */ ``` * 接著因 $1 \leq ix0 < 4$ 表示 $1 \leq \sqrt{ix0} < 2$,使得可以從 `1` 開始逼近 $\sqrt{ix0}$ 的數值。 * 此外前面將 `fraction` 部分擴大至 `24` bit(新增 `1` 的部分),這裡又將 `ix0 += ix0`,因此範圍變成$2 \leq ix0 < 8$,所以為了保證 `ix0` 最小值 `1.fraction` 能正確逼近,這裡要從 `25` 位開始做判斷, QQ0 = `(a) 0x01000000` :::warning 想請問這裡一下,這裡 `ix0 += ix0` 與 `ix0 << 1` 在各種方面上有差異嗎? ::: * 令 $q_i = \sqrt{ix0}$ 取小數點後 $i$ 位的精度$q_0 = 1$, $s_i = 2q_i$且$ix0_i = 2^{i+1} (ix0 - q_i^2)$ * 為了透過$q_i$計算$q_{i+1}$,若$(q_i+2^{-(i+1)})^2 \leq ix0$成立則$q_{i+1} = q{i} + 2^{-(i+1)}(補1) 否則 q_{i+1} = q_i(補0)$ * 接著透過代換得到$s_i+2^{-(i+1)} \leq ix0_i$ * 若成立,$s_{i+1} = s_{i} + 2^{-i},ix0_{i+1} = ix0_i - s_i - 2^{-(i+1)}$ * 否則,$s_{i+1} = s_{i},ix0_{i+1} = ix0_i$ * 這裡以圖解說明逼近過程,首先假設 `ix0 = 1.10...` ```graphviz digraph FP{ node[shape=record] BF16 [label="<f0>0|<f1>0|<f2>0|<f3>0|<f4>0|<f5>0|<f6>0|<f7>0|<f8>1|..."] BF16:f0->Sign BF16:f8->24 } ``` `ix0 += ix0` ```graphviz digraph FP{ node[shape=record] BF16 [label="<f0>0|<f1>0|<f2>0|<f3>0|<f4>0|<f5>0|<f6>0|<f7>1|<f8>1|..."] BF16:f0->Sign BF16:f8->24 } ``` `q = s0 = 0` `r = 0x01000000` `t = s0 + r` 也就是 $s_i+2^{-(i+1)} \leq ix0_i$ 首項 `s` 為 `0` 是因為還沒進入小數位 `t <= ix0` `t = 0x01000000` < `ix0 = 0x018XXXXX` `s0 = t + r` 也就是 $s_{i+1} = s_{i} + 2^{-i}$ `s0 = 0x02000000` `ix0 -= t` 也就是 $ix0_{i+1} = ix0_i - s_i - 2^{-(i+1)}$ `ix0 = 0x008XXXXX` ```graphviz digraph FP{ node[shape=record] BF16 [label="<f0>0|<f1>0|<f2>0|<f3>0|<f4>0|<f5>0|<f6>0|<f7>0|<f8>1|..."] BF16:f0->Sign BF16:f8->24 } ``` `q += r` `q = 0x01000000` `ix0 += ix0` `ix0 = 0x01XXXXXX` ```graphviz digraph FP{ node[shape=record] BF16 [label="<f0>0|<f1>0|<f2>0|<f3>0|<f4>0|<f5>0|<f6>0|<f7>1|<f8>0|..."] BF16:f0->Sign BF16:f8->24 } ``` `r >>= 1` `r = 0x00800000` * 透過 `ix0 += ix0` 對 `ix0` 左移,透過 `r >> 1` 對 `r` 右移,不斷逼近 $\sqrt{ix0}$ 並將逼近後每位數值存於 `q` * 剩下的數值將會根據不同系統上的精度做 `rounding`,四捨五入 ```cpp /* generate sqrt(x) bit by bit */ ix0 += ix0; q = s0 = 0; /* [q] = sqrt(x) */ r = QQ0; /* 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; r >>= 1; } /* use floating add to find out rounding direction */ if (ix0 != 0) { z = one - tiny; /* trigger inexact flag */ if (z >= one) { z = one + tiny; if (z > one) q += 2; else q += (q & 1); } } ``` * 最後將計算結果還原成浮點數表示 * `q` 表示的是小數點落在 24~25 bit間的編碼,因此右移一次回到正常範圍 * QQ1 = `(d) 0x3f000000`,照理來講應該加上 `0x3f800000` ,但因 `q = 1.fraction` 其最高位的 `1` 右移時補上了缺口 * `m` 表示 `exponent` 部分,QQ2 = `(g) 23` 左移回正確位置 ```cpp ix0 = (q >> 1) + QQ1; ix0 += (m << QQ2); INSERT_WORDS(z, ix0); return z; ``` ### 延伸問題 [LeetCode 69. Sqrt(x)](https://leetcode.com/problems/sqrtx/) ```cpp typedef union { float value; uint32_t v_int; } ieee_float_shape_type; #define EXTRACT_WORDS(ix0, d) \ do { \ ieee_float_shape_type ew_u; \ ew_u.value = (d); \ (ix0) = ew_u.v_int; \ } while (0) int mySqrt(int x) { int32_t sign = 0x80000000; int32_t ix0, m, i; EXTRACT_WORDS(ix0, x); /* take care of zero */ if (ix0 <= 0) { if ((ix0 & (~sign)) == 0) return x; /* sqrt(+-0) = +-0 */ if (ix0 < 0) return (x - x) / (x - x); /* sqrt(-ve) = sNaN */ } /* normalize x */ m = (ix0 >> 23); if (m == 0) { /* subnormal x */ for (i = 0; (ix0 & 0x00800000) == 0; i++) ix0 <<= 1; m -= i - 1; } m -= 127; /* unbias exponent */ ix0 = (ix0 & 0x007fffff) | 0x00800000; if (m & 1) { /* odd m, double x to make it even */ ix0 += ix0; } m >>= 1; /* m = [m/2] */ unsigned int lo = 1 << m, hi = 1 << (m + 1); unsigned int mid = (lo + hi) >> 1; while(1){ if(x > (mid + 1) * (mid + 1)){ lo = mid; mid = (lo + hi) >> 1; } else if (x < mid * mid){ hi = mid; mid = (lo + hi) >> 1; } else return mid; } } ``` ![](https://i.imgur.com/UxDAwka.png) 因本題無須深入求小數點位的精度要求,因此借用本題方式轉換為$2^{\frac{m}{2}} \times 1.fraction$接著利用$2^{\frac{m}{2}}$做 `Binary search` 以此快速縮小範圍,最終回傳 `mid`。 ## 3. ```cpp int consecutiveNumbersSum(int N) { if (N < 1) return 0; int ret = 1; int x = N; for (int i = 2; i < x; i++) { x += ZZZ; if (x % i == 0) ret++; } return ret; } ``` 給定正整數N,求N能寫成多少種連續正整數的和,假設從 `x` 開始,且個數共有 `k` 個 根據等差數列的求和公式 $$ kx = N - \frac{(k-1)k}{2} $$ * 求有幾種組合`k` 與 `x` 能滿足 `N - {0 ~ k}的和 = kx` * 首先看到 `x = N` 對應著公式中的 `N`,代表 `ZZZ` 對應著 `{0~k}的和`,因此 `i` 對應 `k` * `for` 迴圈則是用來計算不同終點的等差數列,所以ZZZ = `(d) 1 - i` * 舉例來說,若 `N = 3` * 第一個迴圈`N - {0~1}的和 = 2` 也就是 `x += 1 - i`,因此檢查 `x % i` 也就是$x = \frac{N - \frac{(k-1)k}{2}}{k}$就能知道是否有可行的解 * 第二個迴圈`N - {0~2}的和 = 0` 因 `x` 於第一個迴圈已被更新,因此算出結果仍然等於 `x += 1 - i`,且 `x % i == 0` 因此得到 `x = 0` 的整數解一個,以此類推,直到 `k = x`。