# 2022q1 Homework2 (quiz2) contributed by < `robertnthu` > ###### tags: `linux2022` # 測驗 1 >EXP1 = a & b & 1 EXP2 = a & b EXP3 = a ^ b ## 解釋運作原理 **EXP1** ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (EXP1); } ``` * 因為 `a >> 1`就是 `a / 2`,`b >> 1` 就是 `b / 2` * 只要 a 不是奇數且 b 不是奇數,那`(a >> 1) + (b >> 1)`都會是`(a + b) / 2` * 如果 a ,b 都是奇數,那我們應該多加一個1在`(a >> 1) + (b >> 1)`後,其餘情況都是0 * 這個判斷可以用`a & b & 1`來實現 * a, b 的第一個 bit 都為1時,`a & b & 1` = 1 **EXP2, EXP3** 參考:[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84-C-%E8%AA%9E%E8%A8%80%EF%BC%9A%E6%95%B8%E5%80%BC%E7%B3%BB%E7%B5%B1%E7%AF%87) * 用加法器來思考: x & y 是進位, x ^ y 是位元和, / 2 是向右移一位 * 位元相加不進位的總和: x ^ y * 位元相加產生的進位值: (x & y) << 1 * x + y = x ^ y + ( x & y ) << 1 * 所以 (x + y) / 2 = (x + y) >> 1 = (x ^ y + (x & y) << 1) >> 1 = (x & y) + ((x ^ y) >> 1) ## 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 ### 輸出組合語言 程式 ```c #include <stdio.h> #include <stdint.h> unsigned int avg(unsigned int a, unsigned int b) { return a + (b - a) / 2; } int main(void) { unsigned int x, y; scanf("%u", &x); scanf("%u", &y); avg(x, y); return 0; } ``` **組合語言** 利用 `gcc -O2 -S test.c`來編譯程式,`-O2`是開啟最佳化,取出程式片段 1. b + (a - b) / 2 ``` movl %esi, %eax subl %edi, %eax shrl %eax addl %edi, %eax ``` * `%esi` 存的是 a 的值,把 a 的值放到 `%eax`, 而 b 放在 `%edi` * `subl %edi %eax` 是 `%eax = %eax - %edi`,也就是 a - b 的運算 * `shrl %eax` shift right,是 (a - b) / 2 的運算 * `addl %edi, %eax`再加上 b ,就完成了 2. (a >> 1) + (b >> 1) + (a & b & 1) ``` movl %edi, %eax movl %esi, %edx andl %esi, %edi shrl %eax shrl %edx andl $1, %edi addl %edx, %eax addl %edi, %eax ``` 3. (a & b) + ((a ^ b) >> 1) ``` movl %edi, %eax andl %esi, %edi xorl %esi, %eax shrl %eax addl %edi, %eax ``` 以 instruction 數量來說,`b + (a - b) / 2`是最少的 但是它有一個`addl`以及`subl`指令 而`(a & b) + ((a ^ b) >> 1)`雖然多一個 instruction,但是只有一個`addl`指令。 ## 研讀 Linux 核心原始程式碼 * `BUILD_BUG_ON`: 參考[linux tricks 之 BUILD_BUG_ON_ZERO.](https://www.cnblogs.com/3me-linux/p/6210573.html#:~:text=BUILD_BUG_ON%EF%BC%9A%E5%8F%AA%E6%9C%89%E6%9D%A1%E4%BB%B6condition%E4%B8%BA0%E6%97%B6%E5%8F%AF%E7%BC%96%E8%AF%91%EF%BC%8C%E4%BD%86%E4%B8%8D%E8%BF%94%E5%9B%9E%E5%80%BC%EF%BC%8C%E5%90%A6%E5%88%99%E7%BC%96%E8%AF%91%E5%99%A8%E6%8A%A5%E9%94%99%E3%80%82,BUILD_BUG_ON_ZERO%EF%BC%9A%E5%8F%AA%E6%9C%89%E6%9D%A1%E4%BB%B6e%E4%B8%BA0%E6%97%B6%E8%BF%94%E5%9B%9E0%EF%BC%8C%E5%90%A6%E5%88%99%E7%BC%96%E8%AF%91%E5%99%A8%E6%8A%A5%E9%94%99%E3%80%82%20%E6%80%BB%E7%BB%93%EF%BC%9ABUILD_BUG_ON%E5%92%8CBUILD_BUG_ON_ZERO%E4%BD%9C%E7%94%A8%E7%B1%BB%E4%BC%BC%EF%BC%8C%E9%83%BD%E6%98%AF%E5%9C%A8%E5%8F%82%E6%95%B0%E4%B8%BA%E9%9D%9E0%E6%97%B6%E7%BC%96%E8%AF%91%E6%8A%A5%E9%94%99%EF%BC%8C%E4%BD%86%E6%98%AFBUILD_BUG_ON_ZERO%E5%8F%AF%E4%BB%A5%E8%BF%94%E5%9B%9E0%E5%80%BC%EF%BC%8CBUILD_BUG_ON%E4%B8%8D%E5%8F%AF%E3%80%82) ```c include/linux/kernel.h /* Force a compilation error if condition is true */ #define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)])) ``` 這個巨集是為了檢查 `condition`這個數,如果 `condition == 0`會回傳 1,若`condition != 0`則會回傳 -1 * `WRITE_ONCE` ```c #define WRITE_ONCE(var, val) (*((volatile typeof(val) *)(&(var))) = (val)) ``` 參考[Linux内核的WRITE_ONCE函数分析](https://blog.csdn.net/czhzasui/article/details/79679758),`volatile`可以確保這個指令不會因為編譯器最佳化而省略,為了解決編譯時同步問題的方法 **average.h** * 需要三個參數,一個是要被拿來計算的資料結構的名字,一個是浮點數運算的精度,一個是計算 EWMA 時的加權係數 >The third argument, the weight reciprocal, **determines how the new values will be weighed vs. the old state, new values will get weight 1/weight_rcp and old values 1-1/weight_rcp.** Note that this parameter must be a power of two for efficiency. 所以 new values 要乘上 1/weight_rcp,old values 要乘上 1-1/weight_rcp,兩個相加就是 EWMA。 這個程式是最主要的計算,internal 就是 old values ```c WRITE_ONCE(e->internal, internal ? \ (((internal << weight_rcp) - internal) + \ (val << precision)) >> weight_rcp : \ (val << precision)); ``` 為了計算方便,令 weight_rcp 是 2 的冪,這樣乘法運算只需要用 shift right 跟 shift left 就可以完成 # 測驗 2 >EXP4 = a ^ b >EXP5 = a <= b ---- 回顧[解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation#%E7%AC%AC%E4%B8%89%E9%83%A8%E5%88%86%EF%BC%9A%E5%9C%A8%E8%B3%87%E8%A8%8A%E5%AE%89%E5%85%A8%E7%9A%84%E6%87%89%E7%94%A8) ```c int32_t min(int32_t a, int32_t b) { int32_t diff = (a - b); return b + (diff & (diff >> 31)); } ``` - 先計算 a, b 的 difference - `(diff >> 31)` - 如果 diff 是正數,則得到 0 ; `diff & (diff >> 31)` 就是 0 - 如果 diff 是複數,則得到 -1 (0xFFFFFFFF),`diff & (diff >> 31)` 就還是 `diff` - 總結 - 如果 a > b ,則回傳 b,也就是 b +0 - 如果 a < b ,則回傳 a,也就是 b + (a - b) = b + diff = a ---- ## 程式運作 題目要求是以下程式 ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` 接下來根據提示來回答,希望的操作是 `max(a, b) = (a > b) ? a : b` 1. EXP5 是 a, b 的比較操作 * 比較操作的回傳值是 0 或 1 * EXP4 & -(0) 等於 EXP4 & 0 = 0 * EXP4 & -(1) 等於 EXP4 & 0xFFFFFFFF = EXP4 **所以可以判斷出 EXP4 & ~(EXP5) 等於 EXP4 或 0** 2. 假設 `EXP4 & -(EXP5)` 等於 0,此時 EXP5 = 0 * 則回傳 `a ^ 0` = a,所以是 a > b 的結果 * 我們希望 a > b 時,EXP5 = 0 * EXP5 : a <= b 3. 假設`EXP4 & -(EXP5)`等於 EXP4,此時 EXP5 = 1 * 則回傳 `a ^ EXP4` * 所以我們希望`a ^ EXP4`等於 b * 因為 a ^ a = 0,且 0 ^ b = b * 所以 EXP4 : a ^ b ## 針對 32 位元無號/有號整數的 branchless 實作 :::danger 我認為在不考慮「uint32_t 會出現負數」的前提下,兩者的實作是一樣的。Assign 一個負數給 unsigned int,在 "<=" 的判斷會出錯。負數因為 sign bit 的關係,會判斷為「負數 > 正數」 ::: ## 在 Linux 核心原始程式碼中,找出更多類似的實作手法。 **如何利用`git log`檢索** # 測驗 3 >COND = v & (-1) 或 v | 0 >RET = u << shift ## 程式運作 ### 為何 binary 運算可以用減法代替 mod 運算? 我認為這是因為, mod 運算本身可以用減法實現。 如 37 mod 6,如果用`%`可以直接計算出餘數,但是如果用大減小的方式,37 - 6, 31 - 6, 25 - 6, ...,最後一樣可以得到同樣的結果 ```c uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; // if there's one 0, return u | v. Simplify the condition statement int shift; for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; // right shift until "u | v" has no 0 in 1-bit } while (!(u & 1)) // shift right until 1-bit in u is not 0 u /= 2; // Because if u = 2^k, v = 2^h, we can divide 2^min(k, h) do { while (!(v & 1)) // shift right until 1-bit in v is not 0 v /= 2; if (u < v) { v -= u; // if u < v, v = v - u } else { // u >= v uint64_t t = u - v; u = v; // u = v v = t; // v = u - v } } while (COND); // gcd will keep going until v == 0 or v == 1 return RET; // it should be u, but i can't garantee } ``` 1. `if (!u || !v) return u | v;` 檢查 u, v 是否都非零 2. 如果 u, v 的二進位有連續的 0,那就一起 shift,而最後還必須要把 shift 再乘回來,因為這也是最小公倍數的一部分 ``` u = 1 1 0 0 0 0 v = 1 0 1 0 0 0 || \/ u = 1 1 0 v = 1 0 1 ``` 3. 接著是輾轉相除法 ```c while (!(u & 1)) u /= 2; ``` :::info **為什麼要把 u, v 各自 shift 到 1-bit 不為 0?** * 因為一個奇數與一個偶數的最小公倍數,必定不會是偶數,所以可以直接把偶數 shift right,相當於除以2,直到出現奇數。 * 假設經過一開始的 shift 後,u = 10011, v = 11000,此時因為 u 是奇數,gcd(u, v)不可能會是2, 4, 8,所以 v 可以直接 shift right 3 bits ::: * 接著是一般的輾轉相除法,如果 u > v,則 gcd(u, v) = gcd(v, u - v),後續的程式就是這裡的實作 ```c if (u < v) { v -= u; // if u < v, v = v - u } else { // u >= v uint64_t t = u - v; u = v; // u = v v = t; // v = u - v } ``` * 因為上面的條件判斷方式,最後 v 一定是較小的數,所以迴圈的中止條件是 `v == 0`,用 bitwise 操作就是 `v & (-1)` 或是 `v | 0`。而中止迴圈後要回傳 u,再乘上一開始 shift 的 bits 數 ```c uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; // if there's one 0, return u | v. Simplify the condition statement int shift; for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; // right shift until "u | v" has no 0 in 1-bit } while (!(u & 1)) // shift right until 1-bit in u is not 0 u /= 2; // Because if u = 2^k, v = 2^h, we can divide 2^min(k, h) do { while (!(v & 1)) // shift right until 1-bit in v is not 0 v /= 2; if (u < v) { v -= u; // if u < v, v = v - u } else { // u >= v uint64_t t = u - v; u = v; // u = v v = t; // v = u - v } } while (v & (-1)); // gcd will keep going until v == 0 return u << shift; } ``` ## 在 x86_64 上透過 __builtin_ctz 改寫 GCD * `int __builtin_ctz(unsigned int)` * This builtin method by GCC determines the count of trailing zero in the binary representation of a number. * from [Builtin GCC Functions - __builtin_clz(); __builtin_ctz(); __builtin_popcount();](https://www.go4expert.com/articles/builtin-gcc-functions-builtinclz-t29238/) * 所以我們可以利用 `__builtin_ctz` 來取代計算 trailing zero 的迴圈 ### 計算 shift ```c for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; // right shift until "u | v" has no 0 in 1-bit } ``` 且這裡 u, v 的 right shift 可以移到後面再做,所以改寫為 ```c shift = min(__builtin_ctz(u), __builtin_ctz(v)); ``` 使用先前「不需要分支的 min」 ```c int64_t min(int64_t a, int64_t b) { int32_t diff = (a - b); return b + (diff & (diff >> 31)); } ``` ---- 後續的 ```c while (!(u & 1)) // shift right until 1-bit in u is not 0 u /= 2; ``` 以及 ```c while (!(v & 1)) // shift right until 1-bit in u is not 0 v /= 2; ``` 也改寫成 ```c u >>= __builtin_ctz(u); ``` 以及 ```c v >>= __builtin_ctz(v); ``` **簡化條件判斷** 由於條件判斷的部份看起來可以更簡潔,配合先前的 min(), max() 改寫 **最終程式** ```c int64_t min(uint64_t a, uint64_t b) { int32_t diff = (a - b); return b + (diff & (diff >> 31)); } int64_t max(uint64_t a, uint64_t b) { int32_t diff = (a - b); return a - (diff & (diff >> 31)); } uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int shift; shift = min(__builtin_ctz(u), __builtin_ctz(v)); u >>= __builtin_ctz(u); do { v >>= __builtin_ctz(v); int64_t tmp = min(u, v); v = max(u, v) - tmp; u = tmp; } while (v & (-1)); // gcd will keep going until v == 0 return u << shift; } ``` ### 測試效能 未完成 ## lib/math/gcd.c * `__ffs` 作用跟`__builtin_ctz`一樣,參考[__ffs.h](https://elixir.bootlin.com/linux/v4.19.55/source/include/asm-generic/bitops/__ffs.h#L13) * 但是定在 <string.h> 的 `ffs`跟 [__ffs.h](https://elixir.bootlin.com/linux/v4.19.55/source/include/asm-generic/bitops/__ffs.h#L13)裡面的作用不一樣,`ffs`會多計算一位 ```c #if !defined(CONFIG_CPU_NO_EFFICIENT_FFS) ...gcd()... #else ...gcd()... ``` 這裡有兩種 gcd 的程式,如果`__ffs`無法使用,則用第二種 `gcd()` #### gcd() with __ffs available :::spoiler 完整程式 unsigned long gcd(unsigned long a, unsigned long b) { unsigned long r = a | b; if (!a || !b) return r; b >>= __ffs(b); if (b == 1) return r & -r; for (;;) { a >>= __ffs(a); if (a == 1) return r & -r; if (a == b) return a << __ffs(r); if (a < b) swap(a, b); a -= b; } } ::: ```c unsigned long r = a | b; if (!a || !b) return r; ``` 如果 a 或 b 有 0 ,則回傳 r ,因為 r = a | b,所以不管 a, b 中有一個 0 還是兩者都是 0 ,都能回傳正確的值 ```c b >>= __ffs(b); if (b == 1) return r & -r; ``` * `b >>= __ffs(b);` 跟先前使用 `__builtin_ctz` 的`u >>= __builtin_ctz(u)`作用一樣,都是針對 trailing zero 做 shift right * 若 `b == 1`,則代表 `b` 為 2 的冪,`b`的公因數只有 2 ,所以只需要確認 a 是否也有因數為 2 ,就能回傳,2 以外因數可以不用在乎,因為必定不是公因數 * 而檢查 a 有因數為 2 的方法,就是看 a | b 有幾個 trailing zero, * `r & -r` = `(a | b) & (~(a | b) + 1)` `r = a | b`,所以 a, b 的共同 trailing zero 也會是 r 的 trailing zero * 而 `-r`首先會取 compliment,所以本來的 trailing zero 會變成 1 ,再加 1 之後會全部進位變成 0,且除了進位的那個 carry bit 以外的 bit,在 `r & -r`運算之後都會變成 0 ,也就得到想要的最大公因數 ``` // 舉例 a = 1 1 0 1 1 0 0 0 b = 0 0 1 0 0 0 0 0 r = 1 1 1 1 1 0 0 0 -r = 0 0 0 0 1 0 0 0 r & -r = 0 0 0 0 1 0 0 0 // 舉例 a = 1 0 0 1 0 0 1 0 b = 0 0 1 0 0 0 0 0 r = 1 0 1 1 0 0 1 0 -r = 0 1 0 0 1 1 1 0 r & -r = 0 0 0 0 0 0 1 0 ``` :::info 這個操作等價於 ```c if (b == 1) return 1 << min(__ffs(a), __ffs(b)); ``` 但這裡選擇的是`r & -r`的做法 ::: ```c for (;;) { a >>= __ffs(a); if (a == 1) return r & -r; if (a == b) return a << __ffs(r); if (a < b) swap(a, b); a -= b; } ``` * 一開始的`a >>= __ffs(a);`與先前的`v >>= __builtin_ctz(v);`作用一樣 * 因為 b 已經先 shift right 變成奇數,所以如果 a 是偶數,2 也必定不是公因數,所以把 a 也做 shift right,直到 a 也變成奇數 ```c if (a == 1) return r & -r; ``` * 若 a shift right 之後等於 1,則此時的 a 為 2 的冪,與前面對 b 的操作一樣,若 a, b 兩數有一數為 2 的冪,則最大公因數必是 2 的冪 ```c if (a == b) return a << __ffs(r); ``` * 若 `a == b`,此時就是最大公因數,因為 a, b 兩數的trailing zero 已經被 shift right 消去了,所以回傳時要再 shift left 回來,這裡等價於先前的 `return u << shift` ```c if (a < b) swap(a, b); a -= b; ``` * 把比較小的數放到 b ,然後 a = a - b :::success 我認為這個程式與一開始使用 `int shift`紀錄共同 trailing zero 以及`__builtin_ctz`的程式是一樣的,只是實作方式有些微的不同 ::: #### gcd() without __ffs :::spoiler 完整程式 ```c unsigned long gcd(unsigned long a, unsigned long b) { unsigned long r = a | b; if (!a || !b) return r; /* Isolate lsbit of r */ r &= -r; while (!(b & r)) b >>= 1; if (b == r) return r; for (;;) { while (!(a & r)) a >>= 1; if (a == r) return r; if (a == b) return a; if (a < b) swap(a, b); a -= b; a >>= 1; if (a & r) a += b; a >>= 1; } } ``` ::: ```c unsigned long r = a | b; if (!a || !b) return r; /* Isolate lsbit of r */ r &= -r; ``` * 這裡把 r 存成 r & -r,也就是先前說明過的一個公因數且為 2 的冪 ```c while (!(b & r)) b >>= 1; if (b == r) return r; ``` * 此處把 b 向右 shift,直到 b 的 first set bit 跟 r 對在一起 ```c // 本來 r = 0 0 0 0 1 0 0 0 b = 1 0 1 0 0 0 0 0 // 之後 r = 0 0 0 0 1 0 0 0 b = 0 0 1 0 1 0 0 0 ``` * 如果 b shift right 之後等於 r ,則 b 也是 2 的冪,且我們已經找到一個 r 是 a, b 的公因數是 2 的冪,那 r 就必然是最大公因數,因為 b 既然是 2 的冪,b 就不會有 2 以外的因數 ```c for (;;) { while (!(a & r)) a >>= 1; if (a == r) return r; if (a == b) return a; if (a < b) swap(a, b); a -= b; a >>= 1; if (a & r) a += b; a >>= 1; } ``` * 首先也是對 a 做一樣的處理,向右 shift 到跟 r 對齊,並檢查 a 是否也是 2 的冪,如果是則回傳 r * 若此時`a == b`, a 就是 a, b 的最大公因數,則回傳 * 如果 `a < b`,則 a, b兩者交換,把比較大的數放在 a,較小的數放在 b * 接下來的操作較先前兩個較為不同,先`a -= b`,然後 a 向右 shift 一位 * 因為經過先前的操作,a, b兩者的 first set bit 都對齊在 r 的 first set bit,所以相減之後,在該位置必定是 0 ```c r = 0 0 0 0 1 0 0 0 b = 0 0 1 0 1 0 0 0 a = 0 1 0 1 1 0 0 0 // 相減之後 r = 0 0 0 0 1 0 0 0 b = 0 0 1 0 1 0 0 0 a = 0 0 1 1 0 0 0 0 // shift right 之後 r = 0 0 0 0 1 0 0 0 b = 0 0 1 0 1 0 0 0 a = 0 0 0 1 1 0 0 0 ``` * 為何檢查 `a & r`且相等的話再`a += b` ? * 這樣代表 a - b 會是 r 的倍數,因為是 shift right 之後跟 r 相等 # 測驗 4 >EXP6 = bitset & -bitset ## 程式運作原理 ### naive() :::spoiler naive() ```c #include <stddef.h> size_t naive(uint64_t *bitmap, size_t bitmapsize, uint32_t *out) { size_t pos = 0; for (size_t k = 0; k < bitmapsize; ++k) { uint64_t bitset = bitmap[k]; size_t p = k * 64; for (int i = 0; i < 64; i++) { if ((bitset >> i) & 0x1) out[pos++] = p + i; } } return pos; } ``` ::: * bitmap 是一個 uint64_t 的一維 array,每個 uint64_t 代表了 64 個 bits 的資訊 * bitmapsize 是 bitmap 的元素個數 * out 是一個 uint32_t 的一維 array * 這個程式把整個 bitmap 出現的 1 的位置都存在 out 裡面,並回傳整個 bitmap 的 1 的數量 ```c // e.g // bitmap bitmap[0] = 0000 0000 ... 0000 1101 bitmap[1] = 0000 0000 ... 0100 1000 . . // naive() out[0] = 0 out[1] = 2 out[2] = 3 out[3] = 67 out[4] = 70 . . ``` ### improved() :::spoiler improved() ```c #include <stddef.h> size_t improved(uint64_t *bitmap, size_t bitmapsize, uint32_t *out) { size_t pos = 0; uint64_t bitset; for (size_t k = 0; k < bitmapsize; ++k) { bitset = bitmap[k]; while (bitset != 0) { uint64_t t = EXP6; int r = __builtin_ctzll(bitset); out[pos++] = k * 64 + r; bitset ^= t; } } return pos; } ``` ::: >其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 000101,那 t 就會變成 000001,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。 根據測驗 3 [lib/math/gcd.c](https://github.com/torvalds/linux/blob/master/lib/math/gcd.c) 的程式,我們知道 `r & -r`可以有**留下 first set bit 並把其他 bits 都設為 0**的作用,所以這題答案就是`bitset & -bitset` ### 真實案例 [Linux 核心設計: 不只挑選任務的排程器](https://hackmd.io/@sysprog/linux-scheduler#)談到[Linux 2.6 O(1) 排程器](https://hackmd.io/@sysprog/linux-scheduler)時,使用 140-bits bitmap 來作為 priority array,並且使用 find first set 來找出目前 priority 最高的 process。 在這樣的結構裡,這個程式可以找到所有需要被 schedule 的所有優先權級別 # 測驗 5 >PPP = pos - - MMM = list_add EEE = &heads[remainder % size] ## 程式說明 ```c struct rem_node { int key; int index; struct list_head link; }; ``` 這個資料結構是為了使用 hash table 而設計的。 ```c static int find(struct list_head *heads, int size, int key) { struct rem_node *node; int hash = key % size; list_for_each_entry (node, &heads[hash], link) { if (key == node->key) return node->index; } return -1; } ``` 配合 `find()` ,可以在 hash table 裡面找到相對應的 key ,如果有找到同樣的 key 的話,會回傳 index。 :::spoiler `fractionToDecimal()` ```c char *fractionToDecimal(int numerator, int denominator) { int size = 1024; char *result = malloc(size); char *p = result; if (denominator == 0) { result[0] = '\0'; return result; } if (numerator == 0) { result[0] = '0'; result[1] = '\0'; return result; } /* using long long type make sure there has no integer overflow */ long long n = numerator; long long d = denominator; /* deal with negtive cases */ if (n < 0) n = -n; if (d < 0) d = -d; bool sign = (float) numerator / denominator >= 0; if (!sign) *p++ = '-'; long long remainder = n % d; long long division = n / d; sprintf(p, "%ld", division > 0 ? (long) division : (long) -division); if (remainder == 0) return result; p = result + strlen(result); *p++ = '.'; /* Using a map to record all of reminders and their position. * if the reminder appeared before, which means the repeated loop begin, */ char *decimal = malloc(size); memset(decimal, 0, size); char *q = decimal; size = 1333; struct list_head *heads = malloc(size * sizeof(*heads)); for (int i = 0; i < size; i++) INIT_LIST_HEAD(&heads[i]); for (int i = 0; remainder; i++) { int pos = find(heads, size, remainder); if (pos >= 0) { while (PPP > 0) *p++ = *decimal++; *p++ = '('; while (*decimal != '\0') *p++ = *decimal++; *p++ = ')'; *p = '\0'; return result; } struct rem_node *node = malloc(sizeof(*node)); node->key = remainder; node->index = i; MMM(&node->link, EEE); *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; } strcpy(p, decimal); return result; } ``` ::: ```c int size = 1024; char *result = malloc(size); char *p = result; if (denominator == 0) { result[0] = '\0'; return result; } if (numerator == 0) { result[0] = '0'; result[1] = '\0'; return result; } ``` 程式的前半段是檢查如果 input 有 0 的情況,並可以直接回傳。 ```c /* using long long type make sure there has no integer overflow */ long long n = numerator; long long d = denominator; /* deal with negtive cases */ if (n < 0) n = -n; if (d < 0) d = -d; bool sign = (float) numerator / denominator >= 0; if (!sign) *p++ = '-'; ``` 處理正負號,把 numerator, denominator 的負號都去掉,並把正確的正負號加在 `*p`,也就是`result`裡面,並且用 n, d 取代本來的 numerator, denominator ```c long long remainder = n % d; long long division = n / d; sprintf(p, "%ld", division > 0 ? (long) division : (long) -division); if (remainder == 0) return result; p = result + strlen(result); *p++ = '.'; ``` * 先進行第一次運算,如果餘數是 0 的話,代表可以整除,回傳答案,不然就把 result 加進答案,並且開始小數運算 * `sprintf()`,C 語言規格書的描述 >The sprintf function is equivalent to fprintf, except that the output is written into an **array** (specified by the argument s) rather than to a stream. A null character is written at the end of the characters written; it is not counted as part of the returned value. If copying takes place between objects that overlap, the behavior is undefined. * 疑問:為何需要確認 division 的正負號?因為前面處理過 n, d 的正負號,此時 n, d 應該都是正的數字才對,而 n / d 如果應該會大於等於 0 才對 ```c /* Using a map to record all of reminders and their position. * if the reminder appeared before, which means the repeated loop begin, */ char *decimal = malloc(size); memset(decimal, 0, size); char *q = decimal; size = 1333; struct list_head *heads = malloc(size * sizeof(*heads)); for (int i = 0; i < size; i++) INIT_LIST_HEAD(&heads[i]); ``` * decimal 是一個 1024 bytes 的位址來紀錄小數部份,並把 q 也指向該位址,q 是除法的商 * 接下來建立 hash table,選擇 1333 當作 hash table 的大小 * 有點好奇 1333 並非是質數,比起其他質數,發生碰撞的機會比較高 ```c for (int i = 0; remainder; i++) { int pos = find(heads, size, remainder); if (pos >= 0) { while (PPP > 0) *p++ = *decimal++; *p++ = '('; while (*decimal != '\0') *p++ = *decimal++; *p++ = ')'; *p = '\0'; return result; } struct rem_node *node = malloc(sizeof(*node)); node->key = remainder; node->index = i; MMM(&node->link, EEE); *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; } ``` * 只要 remainder 不是 0 ,這個 loop 就會一直做下去 * `int pos = find(heads, size, remainder);` * 到 hash table 去找 remainder,如果找到,就代表出現循環小數,則可以回傳答案 * 小數部份的答案放在 decimal 裡面,按照題目要求處理之後,一個一個放進 *p 裡面 * 如果沒有找到一樣的餘數,就建立一個新的 `rem_node`,並把 remainder 當作 key、 i 當作 index 存入 * 此處的 i 是紀錄了這個迴圈執行了第幾次 * `MMM(&node->link, EEE);`是把 `rem_node`存入 hash table 的巨集,所以 MMM = list_add() 或 list_add_tail() 都可以 * EEE 因為是要放進 hash table 的某個 slot,透過前面 `find()`可以知道,hash 值就是 remainder % size,所以 EEE = &heads[remainder % size] * 最後把商存進 q 字串裡,`數字 + '0'`的用法是把0~9的整數換成 char,然後更新 remainder,就是國高中學的除法 ```c *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; ``` * 所以可以判斷出,每次存進去的 index 值 i ,可以用來判斷在第幾位出現循環小數,那 PPP = pos - - * 假設在第 6 次出現循環小數,代表先前除過的 decimal 會有 6 位重複,所以是 pos - - # 測驗 6 >M = _h >X = 0 ### 回顧 container_of ```c /** * container_of() - Calculate address of object that contains address ptr * @ptr: pointer to member variable * @type: type of the structure containing ptr * @member: name of the member variable in struct @type * * Return: @type pointer of object containing ptr */ #ifndef container_of #ifdef __LIST_HAVE_TYPEOF #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) #else #define container_of(ptr, type, member) \ ((type *) ((char *) (ptr) -offsetof(type, member))) #endif #endif ``` * 利用 offsetof 找到 member 與結構起始點的偏移量,再用當前位置扣除,就可以得到整個結構的起始點 ----- ```c /* * ALIGNOF - get the alignment of a type * @t: the type to test * * This returns a safe alignment for the given type. */ #define ALIGNOF(t) \ ((char *)(&((struct { char c; t _h; } *)0)->M) - (char *)X) ``` 根據提示,alignof() 要回傳 t 的長度,也就是 t 佔了幾個 byte * 首先,宣告了一個 struct { } 然後取出個位址,再扣掉`(char *)X`,所以 `(char *)X)`應該要是這個結構的起點,而`(&((struct { char c; t _h; } *)0)->M)`要是這個結構**加上 t 的位址**,這樣相減,才會得到 t 的長度 * M = _h * X = 0 # 測驗 7 >KK1 = div3 KK2 = div5 KK3 = div3 << 2 [Faster remainders when the divisor is a constant: beating compilers and libdivide](https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/) >You have that n/d = n * (2^N^/d) / (2^N^). The division by a power of two (/ (2^N^)) can be implemented as a right shift if we are working with unsigned integers, which compiles to single instruction: that is possible because the underlying hardware uses a base 2. Thus if 2^N^/d has been precomputed, you can compute the division n/d as a multiplication and a shift. > >How do compilers compute the remainder? They first compute the quotient n/d and then they multiply it by the divisor, and subtract all of that from the original value (using the identity n % d = n - (n/d) * d). >The intuition is as follows. To divide by four, you might choose to multiply by 0.25 instead. Take 5 * 0.25, you get 1.25. The integer part (1) gives you the quotient, and **the decimal part (0.25) is indicative of the remainder: multiply 0.25 by 4 and you get 1, which is the remainder.** Not only is this more direct and potential useful in itself, it also gives us a way to check quickly whether the remainder is zero. That is, it gives us a way to check that we have an integer that is divisible by another: do x * 0.25, the decimal part is less than 0.25 if and only if x is a multiple of 4. 要檢查一個數會不會被 4 整除,就把那個數乘上 0.25,然後檢查小數部份,如果小於 0.25,那這個數可以被 4 整除 * `is_divisible`會回傳一個 bool ,如果可以整除的話,則會回傳 1,否則回傳 0 * 由此可知 * div3 = 0, div5 = 0,要印出 數字,所以長度為 2 * div3 = 0, div5 = 1,要印出 "Buzz",長度為 4 * div3 = 1,div5 = 0,要印出 "Fizz",長度為 4 * div3 = 1,div5 = 1,要印出 "FizzBuzz",長度為 8 觀察 `unsigned int length = (2 << KK1) << KK2;` 根據提示,KK1, KK2 都是變數名稱,那就是 div3, div5 了,因為 2 shift left 就是乘以 2 ,再往下看: ```c char fmt[9]; strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (KK3)], length); fmt[length] = '\0'; ``` * div3 = 0,div5 = 0, 要印出數字,希望操作後為 8 * div3 = 1,div5 = 0, 要印出 Fizz ,希望操作後為 0 * div3 = 0,div5 = 1, 要印出 Buzz ,希望操作後為 4 * div3 = 1,div5 = 1, 要印出 FizzBuzz ,希望操作後為 0 div5 = 1時,9 已經 shift right 變成 4 (0100) * 如果 div3 = 1,希望操作後為 0,所以還要在 shift right 至少三個 bits * 如果 div3 = 0,希望操作後為 4,所以不需要 shift 了 div5 = 0時,9 還是 9 (1001) * 如果 div3 = 1,希望操作後為 0(0000),所以還要 shift right 至少四個 bit * 如果 div3 = 0,希望操作後為 8(1000),**此處如果-1,操作無法維持與前述幾個同樣形式**,若把 9 改成 8 ,則不影響前面的 shift 操作,且此處不操作即可滿足條件 KK3 = div3 << 2,因為 1 << 2 = 4,可滿足前述操作