--- tags: linux2022 --- # 2022q1 Homework2 (quiz2) contributed by < `bebo1010` > ## 測驗一 ### 作答思路 #### EXP1 原本程式碼為`return (a >> 1) + (b >> 1) + (EXP1);` 我注意到`a >> 1`和`b >> 1`會導致最後一位元的資料損失 因此EXP1的目標是找回最後一位元的資料 因此我列出底下的truth table 如果a和b都是奇數時,應該要進位上去 如果其中一個不是奇數,不會有進位 根據 truth table 找到應該至少會有`a & b` 接下來再補上`& 1`讓位元對齊最後一位 | a | b | result | | - | - | ------ | | 1 | 1 | 1 | | 1 | 0 | 0 | | 0 | 1 | 0 | | 0 | 0 | 0 | a & b & 1 #### EXP2 a & b & 1 正確答案: a & b 我個人認為,EXP2 和 EXP3 題目出得不是很好 題目本身的提示太少,很難找出一個答題的線索 舉例 EXP1 的題目,給的提示就足夠讓我推敲出可能的答案 EXP4 和 EXP5 ,雖然提示沒有很多,但也可以稍微猜測個可能性 相較之下,EXP2 和 EXP3 的提示非常的少 我下課額外詢問教授為何答案是如此,教授也沒有提到解題的思路 因此我才覺得這兩個題目不是那麼恰當 #### EXP3 a & b 正確答案: a ^ b ### 延伸問題 -- 編譯器開啟優化 #### 第一版本 針對第一版本的 average 我大致撰寫了以下程式 ```c // q1_1.c #include <stdio.h> #include <stdlib.h> #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a + b) / 2; } int main(){ int low = 5; int high = 6; printf("%d\n", average(low,high)); return 0; } ``` 接著把這段程式碼進行編譯,但只做到轉換為組合語言的步驟 編譯方式為: `gcc -O3 -S q1_1.c -masm=intel` 編譯器優化使用 `-O3` ,把組合語言轉成 `intel` 格式 以下內容摘錄 average function 內容 ``` // q1_1.s average: .LFB39: .cfi_startproc endbr64 lea eax, [rdi+rsi] shr eax ret .cfi_endproc ... ``` `lea` 會把 `[rdi+rsi]` 記憶體位置複製進 `eax` `shr` 會把 `eax` 內的值向右位移 `ret` 是 `return` instruction 這樣看下來真的有點奇怪,`shr` 似乎並沒有特別指出要向右位移幾位 那這樣它是怎麼正確計算 average 的? 接著我開始嘗試不同的編譯器優化,發現 `-O1`、`-O2` 的結果都和 `-O3` 完全相同 直到測試到 `-O0` 才看出變化 ``` average: .LFB6: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov DWORD PTR -4[rbp], edi mov DWORD PTR -8[rbp], esi mov edx, DWORD PTR -4[rbp] mov eax, DWORD PTR -8[rbp] add eax, edx shr eax pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc ... ``` 整體看下來是先把原本的兩個值暫存到一個空間後,把這兩個值相加後再向右位移的作法 只是我不了解為何編譯器要做這麼多的 `mov` instruction,找不到一個理由去解釋這個狀況 #### 第二版本 接著針對第二版本的程式碼去了解組合語言的版本 ```c #include <stdint.h> uint32_t average(uint32_t low, uint32_t high) { return low + (high - low) / 2; } ``` 第一次測試一樣先使用 `-O3` 進行測試 ``` average: .LFB0: .cfi_startproc endbr64 mov eax, esi sub eax, edi shr eax add eax, edi ret .cfi_endproc ... ``` 組合語言做出的方法和原本 C 的程式碼看起來是完全相同,沒有任何變化 接著我也測試了 `-O2` 和 `-O1`,和 `-O3` 完全相同 `-O0` 的結果則是有些變化 ``` average: .LFB0: .cfi_startproc endbr64 push rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 mov rbp, rsp .cfi_def_cfa_register 6 mov DWORD PTR -4[rbp], edi mov DWORD PTR -8[rbp], esi mov eax, DWORD PTR -8[rbp] sub eax, DWORD PTR -4[rbp] shr eax mov edx, eax mov eax, DWORD PTR -4[rbp] add eax, edx pop rbp .cfi_def_cfa 7, 8 ret .cfi_endproc ... ``` 雖然說 `-O0` 的是有些變化,但是只是多了一些 `mov` 指令 程式核心完全沒有變化,我可能接下來不會再測試 `-O0` 了 #### 第三版本 ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (a & b & 1); } ``` 一樣透過 `-O3` 進行編譯 ``` average: .LFB0: .cfi_startproc endbr64 mov eax, edi mov edx, esi and edi, esi shr eax shr edx and edi, 1 add eax, edx add eax, edi ret .cfi_endproc ... ``` 假設 `edi` 是 `a`;`esi` 是 `b` 一開始把兩個值先暫存到 `eax` 和 `edx` 接著先做 `and` ,也就是 C 程式碼內的 `(a & b & 1)` 的前半 剩下的部分就只是把其他的計算做完而已 接著也測試了 `-O2` 和 `-O1`,`-O2` 和 `-O3` 的結果相同 `-O1` 的結果則是稍微不同 ``` average: .LFB0: .cfi_startproc endbr64 mov eax, edi shr eax mov edx, esi shr edx add eax, edx and edi, esi and edi, 1 add eax, edi ret .cfi_endproc ... ``` 說是稍微不同,但實際上只是把程式順序稍微對調而已 本質上是沒有差異的 #### 第四版本 ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a & b) + ((a ^ b) >> 1); } ``` 先使用 `-O3` 測試 ``` average: .LFB0: .cfi_startproc endbr64 mov eax, edi and edi, esi xor eax, esi shr eax add eax, edi ret .cfi_endproc ... ``` 組合語言內容和 C 的程式碼幾乎是同個模子刻出來的 接著也測試了 `-O2` 和 `-O1` ,得到和第三版本時完全同樣的結果 以下放上 `-O1` 的內容 ``` average: .LFB0: .cfi_startproc endbr64 mov eax, edi xor eax, esi shr eax and edi, esi add eax, edi ret .cfi_endproc ... ``` 很單純的把執行順序對調,本質上完全沒有差異 ### Exponentially weighted moving average (EWMA) 首先我先去查詢這個演算法的細節是什麼([link](https://corporatefinanceinstitute.com/resources/knowledge/trading-investing/exponentially-weighted-moving-average-ewma/)) 裡面寫到這個算法是一個遞迴式,算式如下 $EWMA_t = \alpha * r_t + (1 - \alpha) * EWMA_{t-1}$ 其中 $\alpha$ 是一個常數(衰退係數,數值為 `0 ~ 1`) $r_t$ 是當下時間的資料數值 但是 [linux](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 的程式碼我真的沒有能力看懂,有著太多不理解的地方 ## 測驗二 ### 作答思路 #### EXP4 a ^ b EXP4 和 EXP5 我是用 trial and error 得知 我一開始先測試 EXP5 為`a > b`後再測試 EXP4 可能的內容 直覺猜測 EXP4 可能為`a ^ b` (考量到進行 XOR 後對於原本資料的損失很小) 發現此時回傳值剛好為較小值 因此把 EXP5 改為`a < b`就得到答案 #### EXP5 a < b ### 延伸問題 -- 有號數的 branchless max 一開始我在嘗試寫的時候,在思考主體應該用 $a > b$ 還是 $a - b$ 針對 $a > b$ 的,我有想出一個 `(a > b) << 31 >> 31` 這個做法可以在 $a > b$ 時讓每個 bit 都設為 1 那接下來再想辦法讓回傳值變成 $a$ 或是 $b$ 因此我撰寫出了以下程式 ```c #include <stdint.h> int32_t max(int32_t a, int32_t b){ return b + ((a - b) & (a > b) << 31 >> 31); } ``` 在 $a > b$ 時,加號右邊的數值會留下 $a - b$ ,再加上 $b$ 就能回傳 $a$ 在 $a < b$ 時,加號右邊的數值會變為 $0$,因此直接回傳 $b$ 用同樣的概念,我也寫出了以下以 $a - b$ 為主體的程式碼 程式碼核心和上面的完全相同 ```c #include <stdint.h> int32_t max(int32_t a, int32_t b){ return a - ((a - b) & (a - b) >> 31); } ``` ## 測驗三 ### 作答思路 #### COND v > 0 這個位置是輾轉相除的中止條件 輾轉相除的中止條件是較小的數(餘數)等於零 因此直接填入`v > 0` 正確答案是`v`,因為`v`的型態是 unsigned int 對於 unsigned int 來說 `v > 0` 和 `v` 是等價的 #### RET u << shift 達成終止條件後的`u`為最大公因數 因此可得知回傳值必定含有`u` 而程式碼前段有針對`u`和`v`縮小數值(持續除以二) 要在這個位置補回去 ### 程式碼解讀 > 註 : GCD 演算法 > 1. If both $x$ and $y$ are $0$, gcd is zero. $\gcd(0,0) = 0$ > 2. $\gcd(x,0) = 0$ and $\gcd(0,y) = 0$ because everything divides $0$. > 3. If x and y are both even, $\gcd(x,y) = \gcd(\frac{x}{2}, \frac{y}{2})$ because $2$ is a common divisor. > Multiplication with $2$ can be done with bitwise shift operator. > 4. If $x$ is even and $y$ is odd, $\gcd(x,y) = \gcd(\frac{x}{2}, y)$. > 5. On similar lines, if $x$ is odd and $y$ is even, $\gcd(x,y) = \gcd(x, \frac{y}{2})$. > it is because $2$ is not a common divisor. ```c= #include <stdint.h> uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int shift; for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } while (!(u & 1)) u /= 2; do { while (!(v & 1)) v /= 2; if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (v); return u << shift; } ``` 第四行的 if 條件對應註釋第一條及第二條,如果兩者其中一個為0,直接 return 0 第五行到第八行對應註釋第三條的前半段,如果兩者皆為偶數就可以把數值縮小並且記錄下目前已經位移幾次 第九行到第十行對應註釋第四條,如果 $u$ 是偶數則做 $u / 2$ 第十二到第十三行對應註釋第五條,如果 $v$ 是偶數則做 $v / 2$ 第二十二行對應註釋第三條的後半段,將原先向右位移掉的數值在這裡用向左位移補回去 ## 測驗四 ### 作答思路 #### EXP6 bitset & -bitset 根據題目的提示 > 其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 `t` 變數。 從這裡我知道我想要的是什麼,然後從教授提供的資源找到我要的程式碼 我找到了`Bit Hack #7. Isolate the rightmost 1-bit.` 這個資料和我想要的程式碼是相同的 因此修改這份資料提供的程式碼後再輸入進去即完成作答 > 引用資源:[Introduction to Low Level Bit Hacks](https://catonmat.net/low-level-bit-hacks) ## 測驗五 ### 作答思路 #### PPP 這位置應該是放入非循環小數部分的判定式,但是我目前找不出判定條件應該是什麼 後來再仔細想想後,發現了一點細節 `int pos = find(heads, size, remainder);` 這行程式碼會回傳第一個重複的小數位置在哪裡,沒有重複就回傳 $-1$ 因此 pos 之前的部分都是非重複的小數,可以使用 pos 當成這個迴圈的計數器 PPP 應該填入 `pos--` #### MMM list_add_tail #### EEE heads MMM 和 EEE 看起來是把當前的小數點數值存入 linked list 的地方 因此 MMM 填入 list_add_tail 讓新的 node 可以放入 list EEE 填入 list 的頭 ### 延伸問題 我想對於處理負數這塊的程式碼進行改進 ```c= /* deal with negtive cases */ if (n < 0) n = -n; if (d < 0) d = -d; bool sign = (float) numerator / denominator >= 0; if (!sign) *p++ = '-'; ``` 針對第二行到第五行,或許可以改成以下 使用 bitwise operator 去處理 ```c if(n & (1 << 31)) n = ~n + 1; if(d & (1 << 31)) d = ~d + 1; ``` 而第七行到第九行或許也可改為以下 直接透過 bitwise operator 取得被除數和除數的 sign bit,進而節省掉除法的使用 ```c uint32_t sign = ((numerator & denominator) & (1 << 31)); if(sign) *p++ = '-'; ``` 接著這份程式還有個問題,decimal 和 heads 的記憶體空間配置後沒有被釋放掉 ```c char *decimal = malloc(size); ``` ```c struct list_head *heads = malloc(size * sizeof(*heads)); ``` ## 測驗六 ### 作答思路 最內層的 `(struct { char c; t _h; } *)` 應該是宣告一個 struct 裡面包含一個字元和一個傳入的 datatype t (可能是 int, double 等等的) 再往外一層的 `((struct { char c; t _h; } *)0)` 我就不理解為何這裡有個 0 最外層的 `(&((struct { char c; t _h; } *)0)->M)` 這裡我也不理解,剛宣告的 struct 為何可以使用 -> ,是不是暗示著這邊的 M 是填入 `c` 或是 `_h`。 接著是 `(char *)(&((struct { char c; t h; } )0)->M)` ,為何這裡又要 cast 成 character pointer? 這個題目對我目前整個就是一個謎團 ## 測驗七 ### 作答思路 #### KK1 和 KK2 KK1 = div3,KK2 = div5 兩者都是用來調整接下來的 strncpy 要使用多少字元 如果不整除 3 也不整除 5 ,length 為 2 如果整除 3 或整除 5 ,length 為 4 如果兩者都整除,length 為 8 #### KK3 div3 << 2 KK3 用來調整 `FizzBuzz%u` 的字串要從哪裡開始複製到 `fmt` 內 要調整的目標為 `(9 >> div5) >> (KK3)` (判定優先順序由上到下) 如果整除 3 ,數值應為 0 如果整除 5 ,數值應為 4 如果不整除 3 也不整除 5 ,數值應該為 8 ### 延伸問題 -- 程式碼解讀 以下為原程式碼加回去答案後的內容 ```c= #include <stdio.h> #include <stdint.h> #include <string.h> #include <stdbool.h> static inline bool is_divisible(uint32_t n, uint64_t M) { return n * M <= M - 1; } static uint64_t M3 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1; static uint64_t M5 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 5 + 1; int main(int argc, char **argv) { for (size_t i = 1; i <= 100; i++) { uint8_t div3 = is_divisible(i, M3); uint8_t div5 = is_divisible(i, M5); unsigned int length = (2 << div3) << div5; char fmt[9]; strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (div3 << 2)], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` 奇怪的問題是,這份程式碼我實際拿去執行後發現在不整除 3 也不整除 5 的狀況下輸出是 u ,而不是原本預期的原數字 進一步測試後發現是程式碼內第二十一行有出錯,把起始點的 9 改為 8 即可正常輸出原數字內容 ```c strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (div3 << 2)], length); ```