# 2024q1 Homework4 (quiz3+4) contributed by < [`jimmylu890303`](https://github.com/jimmylu890303)> ## 第三週測驗題 > [題目](https://hackmd.io/@sysprog/linux2024-quiz3) ### 測驗一 #### 版本一 ```c #include <math.h> int i_sqrt(int N) { int msb = (int) log2(N); int a = 1 << msb; int result = 0; while (a != 0) { if ((result + a) * (result + a) <= N) result += a; a >>= 1; } return result; } ``` 在版本一中,會透過 `log2(N)` 去尋找 N 的最高位元,再從高位逐步去尋找滿足 `(result + a) * (result + a) <= N` 的位元。 以輸入 N = 36 為例, msb = 5 且 a = 100000 | a | result | (result + a) * (result + a) <= N | | ---------- |:------:|:--------------------------------:| | 100000(32) | 0 | X | | 10000(16) | 0 | X | | 1000(8) | 0 | x | | 100(4) | 0 | ==O== | | 10(2) | ==4== | ==O== | | 1(1) | ==6== | X | 所以 result 的值為 6 ,在此版本中使用到 log2 ,所以需要在編譯時多新增編譯選項 `-lm` #### 版本二 在版本二中,去改善版本一使其不依賴 log2 並保持原本的函式原型 (prototype) 和精度 (precision),而改善的作法就是使用 shift 的方式去找出最高位的位元。 ```diff - int msb = (int) log2(N); + int msb = 0; + int n = N; + while (n > 1) { + n >>= 1; + msb++; + } ``` #### 版本三 在版本三中使用 `__builtin_clz(x)` 的巨集來找出最高位在哪個位元算出 leading zero 個數。 ```c int i_sqrt(int x) { if (x <= 1) /* Assume x is always positive */ return x; int z = 0; for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 2) { int b = z + m; z >>= 1; if (x >= b) x -= b, z += m; } return z; } ``` | x | m | z | b | z(after shift) | x >= b | x-=b | z+=m | | --- |:---------:|:---:| --- |:--------------:|:------:| ---- |:----:| | 36 | 10000(16) | 0 | 16 | 0 | ==O== | 20 | 16 | | 20 | 100(4) | 16 | 20 | 8 | ==O== | 0 | 12 | | 0 | 1(1) | 12 | 13 | 6 | X | 0 | 6 | ### 測驗二 針對正整數在相鄰敘述進行 mod 10 和 div 10 操作,如何減少運算成本? 採用 bitwise operation 來實作除法 先考慮除以 10 的情況,除以 10 相當於乘以 $\dfrac{1}{10}$ 但由於 $\dfrac{1}{10}$ 無法以二進位的方式去表示,所以只能透過一個近似的值去替代 而 $\dfrac{1}{10} \approx \dfrac{13}{128}$ ,因為 $\dfrac{128}{13} \approx 9.84$ 所以 x 除以 10 相當於 $x \times \dfrac{13}{128}$ 更進一步拆解上述的式子 $x \times 13 \div 8 \div 16$ ```c unsigned d2, d1, d0, q, r; d0 = q & 0b1; d1 = q & 0b11; d2 = q & 0b111; q = ((((tmp >> 3) + (tmp >> 1) + tmp) << 3) + d0 + d1 + d2) >> 7; r = tmp - (((q << 2) + q) << 1); ``` 考慮以下式子,相當於 $\dfrac{tmp}{8} + \dfrac{tmp}{2} + tmp = \dfrac{13}{8} \times tmp$ ```c ((tmp >> 3) + (tmp >> 1) + tmp) ``` 由於往右作 shift 3個位元會造成最右邊三個位元資訊損失,所以在往左 shift 後要將最右邊三個位元加回來。所以以下操作值會等於 $13 \times tmp$ ```c ((((tmp >> 3) + (tmp >> 1) + tmp) << 3) + d0 + d1 + d2) ``` 所以最後往右 shift 7個位元相當於除上128,這樣就會得到 $tmp \div 10$ 的結果 ```c q = ((((tmp >> 3) + (tmp >> 1) + tmp) << 3) + d0 + d1 + d2) >> 7; ``` 而 tmp 取 10 的餘數則利用餘數定理 $r = f - g \cdot Q$ ```c r = tmp - (((q << 2) + q) << 1); ``` q 為商數,則以下操作為 $q \times 10$,即可算出餘數 r ```c (((q << 2) + q) << 1) ``` 以上的概念可以包裝成以下程式碼 ```c #include <stdint.h> void divmod_10(uint32_t in, uint32_t *div, uint32_t *mod) { uint32_t x = (in | 1) - (in >> 2); /* div = in/10 ==> div = 0.75*in/8 */ uint32_t q = (x >> 4) + x; x = q; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; *div = (q >> 3); *mod = in - ((q & ~0x7) + (*div << 1)); } ``` 在以下操作中, $x = \dfrac{3}{4} \times in$ ```c uint32_t x = (in | 1) - (in >> 2); ``` $q = \dfrac{3}{4} \times in + \dfrac{3}{64} \times in = \dfrac{51}{64} \times in \approx 0.79 \times in$ ```c uint32_t q = (x >> 4) + x; ``` 而這邊的操作是將 0.79 逼近至 0.8 ```c x = q; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; ``` $div = \dfrac{8}{10} \times in \times \dfrac{1}{8} = \dfrac{1}{10} \times in$ ```c *div = (q >> 3); ``` 考慮以下操作 `q >> 3` 為商數 而 `q & ~0x7` 將最低三位清零,相當於商數往左 shift 3位,即為商數的 8 倍 `*div << 1` 為商數的兩倍 `((q & ~0x7) + (*div << 1))` 所以這裡相當於商數乘上 10 ```c *mod = in - ((q & ~0x7) + (*div << 1)); ``` ### 測驗三 #### 版本一 在以下程式碼中,使用右移操作來計算 log 值 ```c int ilog2(int i) { int log = -1; while (i) { i >>= 1; log++; } return log; } ``` 以 8 為例 | round | log | i | i >>= 1 | log++ | | ----- |:---:|:----:|:-------:|:-----:| | 1 | -1 | 1000 | 100 | 0 | | 2 | 0 | 100 | 10 | 1 | | 3 | 1 | 10 | 1 | 2 | | 4 | 2 | 1 | 0 | 3 | 以 7 為例 | round | log | i | i >>= 1 | log++ | | ----- |:---:|:----:|:-------:|:-----:| | 1 | -1 | 0111 | 011 | 0 | | 2 | 0 | 011 | 01 | 1 | | 3 | 1 | 01 | 0 | 2 | 由以上觀察可發現 shift 操作次數是由 msb 在第幾個位元而定,所以這個實作也可以藉由 counting leading zero 來完成 #### 版本二 以下程式碼是將版本一改善,假設輸入資料為 65536 ,在版本一中的 while 迴圈次數需要 65536+1 次,而在版本二中只需要 1 次的迴圈,對於輸入資料很大時,可以減少所需的時間。 ```diff - while (i) { - i >>= 1; - log++; - } + while (i >= 65536) { + result += 16; + i >>= 16; + } + while (i >= 256) { + result += 8; + i >>= 8; + } + while (i >= 16) { + result += 4; + i >>= 4; + } + while (i >= 2) { + result += 1; + i >>= 1; + } ``` #### 版本三 使用 GNU extension `__builtin_clz` 來計算最高位前方有幾個 0 ,在透過 31 - 0 個個數即為最高位元的位置,此值即為 log 的值。 ```c int ilog32(uint32_t v) { return (31 - __builtin_clz(v)); } ``` ### 測驗四 EWMA(指數加權移動平均) 是種取平均的統計手法,並且使經過時間越久的歷史資料的權重也會越低,以下為 EWMA 的數學定義: $S_t = \begin{cases} Y_0, & t=0 \\ \alpha Y_t + (1-\alpha)\cdot S_{t-1}, & t > 0 \end{cases}$ 以下為 ewma 的結構, internal 為 $S_{t-1}$ ,factor 為縮放因子, weight 為權重 ```c /* Exponentially weighted moving average (EWMA) */ struct ewma { unsigned long internal; unsigned long factor; unsigned long weight; }; ``` 考慮以下實作, ```c /** * ewma_add() - Exponentially weighted moving average (EWMA) * @avg: Average structure * @val: Current value * * Add a sample to the average. */ struct ewma *ewma_add(struct ewma *avg, unsigned long val) { avg->internal = avg->internal ? (((avg->internal << avg->weight) - avg->internal) + (val << avg->factor)) >> avg->weight : (val << avg->factor); return avg; } ``` $\alpha = \dfrac{1}{2^{weight}}$ $1- \alpha = \dfrac{2^{weight} - 1}{2^{weight}}$ 若 avg->internal 為 0 ,則為 $2^{factor} \cdot val$ 若 avg->internal 不為 0 ,則為 $\dfrac{2^{weight}-1}{2^{weight}} \cdot internal - \dfrac {2^{factor}}{2^{weight}} \cdot val$ ### 測驗五 以下程式碼計算 $\lceil \log_2 x\rceil$ ```c int ceil_ilog2(uint32_t x) { uint32_t r, shift; x--; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return (r | shift | x > 1) + 1; } ``` 以下用於判別 msb 在左邊 16 位元或右 16 位元,若在左 16 位元則 `x > 0xFFFF` 條件成立,則將 x shift 16位元。 ```c r = (x > 0xFFFF) << 4; x >>= r; ``` 以下用於判別 msb 在這 16 位元中的左 8 位元或右 8 位元,r 用於紀錄 shift 幾個 bits ```c shift = (x > 0xFF) << 3; x >>= shift; ``` 以此類推即可求出 $\lceil \log_2 x\rceil$ 之值 #### 改進程式碼 > [commit - 8d405ff](https://github.com/jimmylu890303/linux2024_lab/commit/8d405ffb572c53874717f777cf3dce9296f7a8ec) 第一個為 x=0 時,做 x-- 會導致 x 值變成 `0xffffffff`,所以作了以下改進 `x -= (!!x)` 會使 x > 0 時才會作減法的動作。 第二個為 x=1 時,應該要回傳 0 的值,但是在原本的程式中會回傳 1 ,所以去檢查 `!(r | shift | x > 0)` 有無成立,若該條件成立代表 x = 0 或者 x = 1 的狀態,再將回傳值清零。 ```diff - x --; + x -= (!!x); - return (r | shift | x > 1)+1; + x = !(r | shift | x > 0) ? 0 : (r | shift | x > 1)+1; + return x; ``` #### ceil_log2 in Linux > [linux/tools/testing/selftests/mm/thuge-gen.c](https://github.com/torvalds/linux/blob/master/tools/testing/selftests/mm/thuge-gen.c#L51) ```c int ilog2(unsigned long v) { int l = 0; while ((1UL << l) < v) l++; return l; } ``` ## 第四週測驗題 > [題目](https://hackmd.io/@sysprog/linux2024-quiz4) ### 測驗一 #### popcount ```c unsigned popcount_naive(unsigned v) { unsigned n = 0; while (v) v &= (v - 1), n = -(~n); return n; } ``` 在最初的 popcount實作中,是使用迴圈將 LSB 的值清 0 , 這邊使用特別操作的手法為 `v &= (v-1)` , 此外 `v &= (v-1)` 的操作也可以用於判斷是否為 2 的冪次方。 ``` 0001 0100 # 20 ; LSB in bit position 2 0001 0011 # 20 - 1 0001 0000 # 20 & (20 - 1) ``` `n = -(~n)` 為 n++ 的操作, $- N = \sim N + 1$ 為二的補數操作,則 $- (\sim N) = N + 1$ #### branchless popcount ```c unsigned popcount_branchless(unsigned v) { unsigned n; n = (v >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; v = (v + (v >> 4)) & 0x0F0F0F0F; v *= 0x01010101; return v >> 24; } ``` 根據以下公式,我們可以計算 popcount(x) $popcount(x) = x - \lfloor \dfrac{x}{2} \rfloor - \lfloor \dfrac{x}{4} \rfloor - \lfloor \dfrac{x}{8} \rfloor - ... - \lfloor \dfrac{x}{2^{31}} \rfloor = \sum\limits_{n=0}^{31}(2^n - \sum\limits_{i=0}^{n-1}2^i)b_n = \sum\limits_{n=0}^{31}b_n$ 以下操作為計算 $x - \lfloor \dfrac{x}{2} \rfloor - \lfloor \dfrac{x}{4} \rfloor - \lfloor \dfrac{x}{8} \rfloor$ `0x77777777` 的設定是因為這邊是以 4 個 bits 為單位 (nibble) ,為了防止另外一組的 nibble 值 shift 過來。 ```c n = (v >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; ``` 所以操作完後的值會結果如下(只考慮最右邊的 nibble),這個 nibble 即代表這 4 個 bits 的 popcount 之值 : $(2^3-2^2-2^1-2^0)b_3 + (2^2-2^1-2^0)b_2 + (2^1-2^0)b_1 + 2^0b_0 = b_3+b_1+b_1+b_0$ ``` 3 2 1 0 b3 (b2-b3) (b1-b2-b3) (b0-b1-b2-b3) ``` 假設 $B_n$ 為 nibble ,所以以下操作可以看出它的結果 ```c v = (v + (v >> 4)) & 0x0F0F0F0F; ``` ``` B7 B6 B5 B4 B3 B2 B1 B0 // v 0 B7 B6 B5 B4 B3 B2 B1 // (v >> 4) // (v + (v >> 4)) B7 (B7+B6) (B6+B5) (B5+B4) (B4+B3) (B3+B2) (B2+B1) (B1+B0) // (v + (v >> 4)) & 0x0F0F0F0F 0 (B7+B6) 0 (B5+B4) 0 (B3+B2) 0 (B1+B0) ``` 最後在透過 `v *= 0x01010101` 將各個 nibble 相加 ``` A6 = B7+B6 A4 = B5+B4 A2 = B3+B2 A0 = B1+B0 0 A6 0 A4 0 A2 0 A0 x 0 1 0 1 0 1 0 1 --------------------------------------------------- 0 A6 0 A4 0 A2 0 A0 0 A6 0 A4 0 A2 0 A0 0 0 A6 0 A4 0 A2 0 A0 0 0 A6 0 A4 0 A2 0 A0 0 --------------------------------------------------- ↑_______________________A6+A4+A2+A0 ``` 可以發現 A6+A4+A2+A0 是我們期望的結果,所以透過 v >> 24 取得。 #### 利用 popcount 計算 Total Hamming distance ```c int totalHammingDistance(int* nums, int numsSize) { int total = 0;; for (int i = 0;i < numsSize;i++) for (int j = 0; j < numsSize;j++) total += __builtin_popcount(nums[i] ^ nums[j]); return total >> 1; } ``` 使用 XOR 運算 可以得出兩數字的差異,在利用巨集 `__builtin_popcount` 即可以算出兩者的 Hamming Distance ,而 Total Hamming distance 就是每個數字都跟其他數字相比,所以時間複雜度會被限制在 $N^2$ 。 #### 改善程式碼 ```c int totalHammingDistance(int* nums, int numsSize) { int total = 0; for(int i=0 ;i<32;i++){ int sum=0; for(int n=0;n<numsSize;n++){ int lsb = (nums[n] & 0x1); sum += lsb; nums[n]>>=1; } total += sum * (numsSize-sum); } return total; } ``` 假設輸入資料為 4, 14, 4 三筆資料,我們從最低位元到最高位元將每個數字的該位元加總,可以發現以下規律 : 該位元的加總 * (數字個數 - 該位元的加總) = 該位元的 Total Hamming distance ,並且時間複雜度為 $N$ ``` numsize = 3 --------------------------------------------- 4 = 0100 14 = 1110 4 = 0100 0th bit : 0 + 0 + 0 = 0 // 0 * (numsize - 0) = 0 1th bit : 0 + 1 + 0 = 1 // 1 * (numsize - 1) = 2 2th bit : 1 + 1 + 1 = 3 // 3 * (numsize - 3) = 0 3th bit : 0 + 1 + 0 = 1 // 1 * (numsize - 1) = 2 total = 0 + 2 + 0 + 2 = 4 ``` ### 測驗二