# [2024q1 Homework3 (quiz3+4)](https://hackmd.io/@sysprog/HkatSCZCT) contributed by < [devarajabc](https://github.com/devarajabc) > ## 第三週測驗 `1-1` ### 1. 解釋上述程式碼運作原理,並嘗試用第 2 週測驗題提到的 ffs / fls 取代 `__builtin_clz`,使程式不依賴 GNU extension。 >參考 [wikipedia](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation) 、[quiz3](https://hackmd.io/@sysprog/linux2024-quiz3) 、[浮點數運算和定點數操作](https://hackmd.io/@NwaynhhKTK6joWHmoUxc7Q/H1SVhbETQ?type=view) #### 解釋程式碼運作原理 在解釋程式碼之前我們要先思考如何計算 $x$ 的平方根? 可以分成以下幾個步驟: ##### 1. 假設 $x$ 的平方根是 $N$ ,則 $x=N^2$ 而其又可再拆解成: $N^2=(a_{n}+a_{n-1}+a_{n-2}+...+{a_0})^2, a_m=2^m\ or\ a_m=0$ 利用乘法公式可將 $(a_{n}+a_{n-1}+a_{n-2}+...+{a_0})^2$ 拆解成: $\begin{split} \\ & a_n^2+[2a_n+a_{n-1}]a_{n-1}+[2(a_n+a_{n-1})+a_{n-2}]a_{n-2}+...+[2(\displaystyle\sum_{i=1}^{n}a_i)+a_0]a_0 \\ \end{split}$ ##### 2. 為了方便計算,使用 $P_m$ 來表示 $(\displaystyle\sum_{i=m}^{n}a_i)$ ,也就是 $P_m=a_n+a_{n-1}+...+a_m$ ,不難發現 $P_0=a_n+a_{n-1}+...+a_0$ 即為平方根 $N$ 至於要如何得到 $P_0$ 呢? 可以將 $P_m$ 寫成以下遞迴式: $\begin{split} \\ P_m = P_{m+1} + a_m\ \end{split}$ 接著利用 $P_m^2 \leq N^2$ 的特性來決定 $a_m=2^m$ 還是 $a_m=0$ 將遞迴式改寫成: \begin{cases} P_m = P_{m+1} + 2^m, & \text{if $P_m^2 \leq N^2$}\\ P_m = P_{m+1}, & \text{otherwise} \end{cases} 遞迴的初始條件如下: $P_n=a_n= 2^n$ , $n$ 為能使 $a_n^2=(2^n)^2= 4^n \leq N^2$ 的最大值。 ##### 3. 若在每次遞迴的過程中都要計算 $N^2 - P_m^2$ 的值會消耗大量的計算資源,因改以變數 $X_m$ 來記錄$N^2 - P_m^2$ 的值,並以遞迴的方式計算差值 $X_m$ ,遞迴式如下: $X_m = N^2 - P_m^2 = X_{m+1} - Y_m$ 其中: $Y_m = P_m^2 - P_{m+1}^2 = 2P_{m+1}*a_m+a_m^2$ 為了方便計算,將 $Y_m$ 分別以 $c_m = 2P_{m+1}*a_m$ 和 $d_m=a_m^2$ 來表示: $Y_m=\left. \begin{cases} c_m+d_m & \text{if } a_m=2^m \\ 0 & \text{if } a_m=0 \end{cases} \right.$ 將 $c_m, d_m$ 稍作整理之後可以得到: * $c_m = P_{m+1}2^{m+1}$ * $d_m = (2^m)^2$ 而 $c_{m},d_{m}$ 可以透過下列遞迴式求得: * $c_{m-1}=(P_{m+1}+a_m)2^m=P_{m+1}2^m+a_m2^m=\begin{cases} c_m/2+d_m & \text{if }a_m=2^m \\ c_m/2 & \text{if }a_m=0 \end{cases}$ * $d_{m-1}=\dfrac{d_m}{4}$ ##### 4. 這個時候 **有 趣 的 事 情** 發生了,當 $m=-1$ 時: $c_{-1} = P_02^0=P_0=N$ 竟然透過計算 $c_m$ 就可以得到所求的平方根 $N$ ,連 $Y_m, X_m$ 之類的都不用管了嗎? 當然不是,遞迴式需要透過 $X_m$ 的值來判斷 $a_m = 0$ 或是 $a_m = 2^m$ ,而 $X_m$ 的值需要透過 $Y_m$ 來計算。 ##### 5. 接著就是如何透過程式碼執行以下遞迴式: * $c_{m-1}=\begin{cases} c_m/2+d_m & \text{if }a_m=2^m \\ c_m/2 & \text{if }a_m=0 \end{cases}$ * $d_{m-1}=\dfrac{d_m}{4}$ 初始條件改為: $\begin{cases} c_n=0 \\ d_n=2^n \end{cases}$ $n$ 為能使 $a_n^2=(2^n)^2= d_n^2 = 4^n \leq N^2$ 的最大值, 此時 $c_n$ 為 $0$ , 為什麼呢? 由於 $P_n=a_n= 2^n$ 且 $P_m = P_{m+1} + 2^m$ 故 $P_{n+1}=0$, $c_n = 0$ ##### 6. 程式運作的流程如下: 1. 利用變數 `x` 來儲存並計算 $X_{m+1} = N^2 - P_{m+1}^2 = N^2$ 2. 利用變數 `m = 1UL << ((31 - __builtin_clz(x)) & ~1UL)` 來記錄 $d_n=2^n$ 3. 利用 $d_{m-1}=\dfrac{d_m}{4}$ 來更新 `m` 4. 利用變數 `b` 來計算 $Y_m$ ,也就是 $c_m+d_m$ 5. 計算 $c_{m-1}=\dfrac{c_m}{2}$ 6. 如果 $X_m$ $>$ $Y_m$ ,代表 $a_m$ 不為零 * 利用 $X_m = X_{m+1} - Y_m$ 更新 $X_m$ 的值 * 將 $c_{m-1}$ 再加上 $d_m$ ```c int i_sqrt(int x) { if (x <= 1) /* Assume x is always positive */ return x; int z = 0;//c_n for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 2) { int b = z + m;//b = y_m, m = d_m z >>= 1; if (x >= b)// x = x_n x -= b, z += m; } return z; } ``` ```shell sqrt_(26) = 5 // 正確答案應為 5.09901951359..... ``` 測試完之後發現問題很大,在計算非完全平方數的平方根時會出現錯誤答案, 錯誤的原因不難理解,因為在程式的運算過程皆以整數 (int) 形態來儲存變數,為了讓輸出更接近真實答案,我打算使用[定點數](https://en.wikipedia.org/wiki/Fixed-point_arithmetic)的技巧來增加精準度。 #### 修改 #### 試用 `ffs / fls` 取代 `__builtin_clz` > 參考 [ffs](https://man7.org/linux/man-pages/man3/ffs.3.html) : > The ffs() function returns the position of the first (least significant) bit set in the word i. The least significant bit is position 1 and the most significant position is, for example, 32 or 64. ### 2. 在 Linux 核心原始程式碼找出對整數進行平方根運算的程式碼,並解說其應用案例,至少該包含 block 目錄的程式碼。 ### 3. 閱讀〈Analysis of the lookup table size for square-rooting〉和 Timing Square Root,嘗試撰寫更快的整數開平分根運算程式。 [quiz-4](https://hackmd.io/@sysprog/linux2024-quiz4)