# 2022q1 Homework2 (quiz2) contributed by `mm0809` ## 測驗 `1` ### 程式運作原理 ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (EXP1); } ``` `(a + b) / 2` 並不能以 `a / 2 + b / 2` 表示。因為若被除數除不盡,則餘數會被省略,導致**先加後除**與**先除後加**結果不同。 | a | b | a/2 + b/2 | (a+b)/2 | | --- | --- | --------- | ------- | | 1 | 2 | 1 | 1 | | 1 | 1 | 0 | 1 | | 2 | 2 | 2 | 2 | | 3 | 3 | 2 | 3 | 當 `a` 和 `b` 都是奇數時就會有誤差出現,因為 `/2` 會有餘數 `1`,而相加以後兩個餘數 1 又可以被 2 除盡。 因此我們可以改寫: `a >> 2 + b >> 2 + (a & b & 1)` 故 `EXP1 = a & b & 1` ```c uint32_t average(uint32_t a, uint32_t b) { return (EXP2) + ((EXP3) >> 1); } ``` 要改寫 `(a + b) / 2` 可以先重 `a + b` 下手,最後再使用 `>> 1` 達到 `/ 2` 的效果。 兩個整數做加法可以想像成有一整排的 full adder 針對每個 bit 做運算。 以 full adder 的觀點來看,我們可以先算 `SUM` 然後再加上 `CARRY IN`。 | a | b | SUM | CARRY OUT | | --- | --- | --- | --------- | | 0 | 0 | 0 | 0 | | 0 | 1 | 1 | 0 | | 1 | 0 | 1 | 0 | | 1 | 1 | 0 | 1 | `SUM` 對應的操作是 `a ^ b`, `CARRY OUT` 對應的操作是 `a & b`。 `CARRY OUT` 是下一個 bit 的 `CARRY IN` ,因此 `CARRY IN` 為 `(a & b) << 1` 。 最後可以得知 `a + b` 為 `(a ^ b) + (a & b) << 1`。 `(a + b) / 2 = (a + b) >> 1 = (a ^ b) >> 1 + a & b` 。 故 `EXP2 = a & b` , `EXP3 = a ^ b`。 ### 比較編譯器最佳化輸出 以下為各版本使用 `gcc -O3 -S` 編譯出來的結果 `版本 1` ```c #include <stdint.h> uint32_t average(uint32_t low, uint32_t high) { return low + (high - low) / 2; } ``` ``` average: // %edi = low, %esi = high endbr64 movl %esi, %eax // high subl %edi, %eax // high - low shrl %eax // (high - low) >> 1 addl %edi, %eax // low + (high -low) >> 1 ret ``` > 相較於 `-O0` 從 `-01` 開始, parameter 不會被 push 到 stack 裡面,作一些不必要的操作。 > ```c > // gcc -O0 -S > average: > endbr64 > pushq %rbp > movq %rsp, %rbp > movl %edi, -4(%rbp) > movl %esi, -8(%rbp) > movl -8(%rbp), %eax > subl -4(%rbp), %eax > shrl %eax > movl %eax, %edx > movl -4(%rbp), %eax > addl %edx, %eax > popq %rbp > ret > ``` `版本 2` ```c #include <stdint.h> uint32_t average(uint32_t low, uint32_t high) { return (low >> 1) + (high >> 1) + (low & high & 1); } ``` ``` average: endbr64 movl %edi, %eax // low movl %esi, %edx // high andl %esi, %edi // low & high shrl %eax // low >> 1 shrl %edx // high >> 1 andl $1, %edi // low & high & 1 addl %edx, %eax // low >> 1 + high >> 1 addl %edi, %eax // low >> 1 + high >> 1 + low & high & 1 ret ``` `版本 3` ```c= #include <stdint.h> uint32_t average(uint32_t low, uint32_t high) { return (low & high) + ((low ^ high) >> 1); } ``` ``` average: endbr64 movl %edi, %eax // low andl %esi, %edi // low & high xorl %esi, %eax // low ^ high shrl %eax // (low ^ high) >> 1 addl %edi, %eax // low & high (low ^ high) >> 1 ret ``` 當編編譯器最佳化開到 `-O3` 時並不會很難看懂,本來以為最佳化開越大越難讀懂,比起 `-O0` 更好讀懂,做得很俐落。可能是因為本題的程式碼短,而且精簡沒有多餘的操作。 ### Linux 核心實作:average.h ```c #define DECLARE_EWMA(name, _precision, _weight_rcp) \ struct ewma_##name { \ unsigned long internal; \ }; \ static inline void ewma_##name##_init(struct ewma_##name *e) \ { \ BUILD_BUG_ON(!__builtin_constant_p(_precision)); \ BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp)); \ /* \ * Even if you want to feed it just 0/1 you should have \ * some bits for the non-fractional part... \ */ \ BUILD_BUG_ON((_precision) > 30); \ BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp); \ e->internal = 0; \ } ``` `name` 用來生成 struct 物件跟建立對應的 helper function。 在 Linux 時做出來的 EWMA 中使用 fixed-precision value 紀錄數值。因此需要指定 `_precision` 。在資料結構中是使用 `unsigned long` 來記錄數值。 `_weight_rcp` 是用來設定 EWMA 公式中的 $\alpha$ ,且為了計算速度, `_weight_tcp` 必須是 power of 2 。 $$ S_t = \begin{cases} Y_0, &t = 0 \\ \alpha Y_t + (1-\alpha)\times S_{t-1}, & t> 0 \end{cases} $$ $$ \alpha = \frac{1}{\_weight\_rcp} $$ ```c static inline unsigned long \ ewma_##name##_read(struct ewma_##name *e) \ { \ BUILD_BUG_ON(!__builtin_constant_p(_precision)); \ BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp)); \ BUILD_BUG_ON((_precision) > 30); \ BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp); \ return e->internal >> (_precision); \ } \ ``` `ewma_##name##read` 這個 helper function 可以讓我們讀出整數部位。 從 `return e->internal >> (_precision)` 可以知道會在輸出時把小數部位去掉。 ```c static inline void ewma_##name##_add(struct ewma_##name *e, \ unsigned long val) \ { \ unsigned long internal = READ_ONCE(e->internal); \ unsigned long weight_rcp = ilog2(_weight_rcp); \ unsigned long precision = _precision; \ \ BUILD_BUG_ON(!__builtin_constant_p(_precision)); \ BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp)); \ BUILD_BUG_ON((_precision) > 30); \ BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp); \ \ WRITE_ONCE(e->internal, internal ? \ (((internal << weight_rcp) - internal) + \ (val << precision)) >> weight_rcp : \ (val << precision)); \ } ``` `ewma_##name##_add` 這個 helper function 把 `val` 加入 EWMA 的計算中。最後一行有一個 Conditional expression `? :` 用來判斷是否為第一個值,並且實現 EWMA 的公式。 $$ S_t = \begin{cases} Y_0, &t = 0 \\ \alpha Y_t + (1-\alpha)\times S_{t-1}, & t> 0 \end{cases} $$ `val << precision` 是把 `val` 轉換為 fixed-precision 的表示。 針對 `((internal << weight_rcp) - internal + val << precision) >> weight_rcp` 可表示成 `internal - internal >> weight_rcp + (val << precision) >> weight_rcp` 把 `>> weight_rcp` 替換成 `_weight_rcp` 可得: $$ internal\times(1 - \frac{1}{\_weight\_rcp}) + \frac{1}{\_weight\_rcp} \times (val\_precision) $$ 符合上述的公式。 這個 macro 被使用在 net 和 gpu 相關的 driver。 本來以為網路相關的是用來決定 TCP timeout 的時間,後來後來查關鍵字 `rssi` , 全名是 Received Signal Strength Indication,跟無線傳輸的訊號強度有關。 gpu 的則是跟 psr(Panel Self-Refresh) 有關,是一種讓螢幕省電的技術。 :::info - [x] 解釋上述程式碼運作的原理 - [x] 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章) - [x] 研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用 ::: ## 測驗 `2` ### 程式運作原理 ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` * 若 `a >= b` 則 `((EXP4) & -(EXP5))` 須為 `0` ,因為 `a ^ 0 = a` * 若 `a <= b` 則 `((EXP4) & -(EXP5))` 須為 `a ^ b` 因為 `a ^ a ^ b = b` 所以 `((EXP4) & -(EXP5))` 只有兩種可能,分別為 `0` 和 `a ^ b`。 可以假設 `EXP4 = a ^ b` 。 又在 `a >= b` 時 `EXP5` 須為 `1` 且在 `a <= b` 時 `EXP5` 須為 `0` ,因此 `EXP5 = a < b or a <= b` 。因為題目要求以最精簡為主因此選擇 `a < b`。 ### 有號整數版本 ```c #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((a ^ b) & -(a < b)); } ``` 遠本版本的實作主要用到兩種操作: * a ^ a ^ b = b * `<` : a < b 回傳 1 , a >= b 回傳 0。 這兩個性質不管是有號或無號數均成立,因此可以沿用原始版本。 :::info - [x] 解釋上述程式碼運作的原理 - [x] 針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作 - [ ] Linux 核心也有若干 branchless / branch-free 的實作,例如 lib/sort.c: ::: ## 測驗 `3` ```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 (COND); return RET; } ``` ```c if (!u || !v) return u | v; ``` 實現了 GCD 演算法中的 * If both x and y are 0, gcd is zero $gcd(0, 0)$ * $gcd(x, 0)=x$ and $gcd(y, 0)=y$ because everything divides 0 ```c int shift; for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` 實現了 GCD 演算法中的 * If x and y are both even, $gcd(x,y)=2\times gcd(\frac{x}{2}, \frac{y}{2})$ because 2 is a common divisor. Multiplication with 2 can be done with bitwise shift operator. 所以最後算出來的最大公因數還要 `<< shift` ```c while (!(u & 1)) u /= 2; ``` 實現了 GCD 演算法中的 * If x is even and y is odd, $gcd(x,y)=gcd(\frac{x}{2},y)$ ```c do { while (!(v & 1)) v /= 2; if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (COND); return RET; ``` 這裡做的演算法與下方的程式類似。只是以 `v -= u` 實現 `v = u % v`。也就是說用減法去取餘數。因此 `COND = v` 。 當 `v` 歸零時 `u` 就會是最大公因數,但因為一開始有考慮到兩數都是偶數時,可以先除 2 ,因此最後的答案還要做修正。 `RET = u << shift` ```c #include <stdint.h> uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; while (v) { uint64_t t = v; v = u % v; u = t; } return u; } ``` ### 用 `__builtin_ctz` 改寫 ```c unsigned int min(unsigned int a, unsigned int b) { return a ^ ((a ^ b) & -(a > b)); } uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int ctzu = __builtin_ctz(u); int shift = min(ctzu, __builtin_ctz(v)); u >>= ctzu; v >>= shift; do { v >>= __builtin_ctz(v); if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (v); return u << shift; } ``` 相較於原始版本,我把所有跟 `/2` 有關的操作都換成 `__builtin_ctz` , 減少 `while` 的判斷。 我使用 perf 去必較效能,驗證新版的效能提升。 ```bash kuan@Athena:~/linux2022/q_w2/q3$ sudo perf stat --repeat 5 ./gcd.out Performance counter stats for './gcd.out' (5 runs): 7,911.68 msec task-clock # 1.000 CPUs utilized ( +- 0.01% ) 1,007 context-switches # 127.331 /sec ( +- 0.35% ) 0 cpu-migrations # 0.000 /sec 45 page-faults # 5.637 /sec ( +- 1.14% ) 26,813,705,619 cycles # 3.389 GHz ( +- 0.00% ) 16,452,869,650 instructions # 0.61 insn per cycle ( +- 0.00% ) 6,851,737,624 branches # 866.028 M/sec ( +- 0.00% ) 1,162,687,024 branch-misses # 16.97% of all branches ( +- 0.01% ) 7.91487 +- 0.00124 seconds time elapsed ( +- 0.02% ) kuan@Athena:~/linux2022/q_w2/q3$ sudo perf stat --repeat 5 ./gcd_ctz.out Performance counter stats for './gcd_ctz.out' (5 runs): 3,640.03 msec task-clock # 1.000 CPUs utilized ( +- 0.05% ) 460 context-switches # 126.318 /sec ( +- 0.30% ) 0 cpu-migrations # 0.000 /sec 44 page-faults # 12.088 /sec ( +- 1.24% ) 12,329,884,870 cycles # 3.387 GHz ( +- 0.00% ) 11,695,305,346 instructions # 0.95 insn per cycle ( +- 0.00% ) 2,173,622,865 branches # 597.144 M/sec ( +- 0.00% ) 371,237,562 branch-misses # 17.08% of all branches ( +- 0.01% ) 3.64181 +- 0.00178 seconds time elapsed ( +- 0.05% ) ``` `gcd_out` 和 `gcd_ctz.out` 分別為原始版本和改良版本。從運行時間可以得知改良版本的效能大約是原始版本的兩倍。再深入觀察可得知改良版的 cycles, branch 和 branch miss 數量大幅下降,印證了我們用 `__builtin_ctz` 把 `while` 取代掉後有助於降低 branch 和運算。 ### linux kernel: lib/math/gcd.c ```c 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; b >>= __ffs(b); if (b == 1) return r & -r; ``` 這裡的 `if` 是用來判斷 `b` 是否是 2 的冪次方,若 `b` 為 2 的冪次方,則 `gcd(a, b)` 必為 2 的冪次方 (1, 2, 4, 8...) 。 > 定義 $U(K) = 2^x$ 為 2 的冪次方中, $2^x$ 為數字 K 的因數,且為最大。 > 且 $U(K)$ 可透過 `K & -K` 取得。因為 `-K = ~K + 1` 最後的 `+ 1` 可以讓 `~k` 中連續的 1 進位,直到遇上第一個 0,此時 0 的位置即為 `K` 第一位為 1 的位置。 因此此時 `gcd(a, b)` 為 $min\,(\,U(a), \, U(b)\,)$ , 換句話說就是要找 `a, b` 兩數第一個 1 的位置的最小值。 因此可先 `r = a | b` 最後再 `return 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; } ``` 最後 for 迴圈的部分跟題目的 while 迴圈類似。遠本題目 while 迴圈的中止條件是 `v == 0` , linux kernel 版本的則是提前判斷 `a == 0` 並回傳值。若 `a == b` 則下一個 cycle `a = 0` , 所以當 `a == b` 回傳 `a << __ffs(r)` ,這裡的 ` << __ffs(r)` 的作用跟題目的 `<< shift` 一樣。 理論上 `if(a == b)` 就足以滿足所有的情況,但是 linux kernel 為了追求效率增加了一個判斷 `a == 1` ,若沒有提前判斷,則還需要多跑 `b` 個 cycle。 我使用 `perf` 來驗證我的想法,結果如下。 ```bash // No if(a == 1) kuan@Athena:~/linux2022/q_w2/q3$ sudo perf stat --repeat 5 ./gcd_kernel.out Performance counter stats for './gcd_kernel.out' (5 runs): 3,469.76 msec task-clock # 1.000 CPUs utilized ( +- 0.03% ) 437 context-switches # 126.003 /sec ( +- 0.42% ) 0 cpu-migrations # 0.000 /sec 45 page-faults # 12.854 /sec ( +- 1.14% ) 11,749,988,006 cycles # 3.386 GHz ( +- 0.00% ) 10,470,285,345 instructions # 0.89 insn per cycle ( +- 0.00% ) 2,271,862,655 branches # 654.761 M/sec ( +- 0.00% ) 391,531,278 branch-misses # 17.23% of all branches ( +- 0.00% ) 3.47131 +- 0.00112 seconds time elapsed ( +- 0.03% ) // with if(a == 1) kuan@Athena:~/linux2022/q_w2/q3$ sudo perf stat --repeat 5 ./gcd_kernel.out Performance counter stats for './gcd_kernel.out' (5 runs): 3,085.58 msec task-clock # 1.000 CPUs utilized ( +- 0.06% ) 390 context-switches # 126.459 /sec ( +- 0.47% ) 0 cpu-migrations # 0.000 /sec 44 page-faults # 14.390 /sec ( +- 0.55% ) 10,449,478,448 cycles # 3.387 GHz ( +- 0.04% ) 10,309,367,245 instructions # 0.99 insn per cycle ( +- 0.00% ) 2,642,511,309 branches # 856.407 M/sec ( +- 0.00% ) 336,945,564 branch-misses # 12.75% of all branches ( +- 0.06% ) 3.08701 +- 0.00201 seconds time elapsed ( +- 0.06% ) ``` ```c // benchmark int main(void) { for (int i = 1; i < 10000; i++) { for (int j = 1; j < 10000; j++) { gcd64(i, j); } } return 0; } ``` 從 perf 測出來的數據來看,雖然 branch 增加但是 branch-misses 並沒有增且,而且跑的 cycle 數更少。 :::info - [x] 解釋上述程式碼運作的原理 - [x] 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升; - [x] Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。 ::: ## 測驗 `4` ```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; } ``` 根據提意,`bitset ^= t` 會把最靠近 `LSB` 的 `1` 換為 `0`。比如說: * 當 `bitset = 0x00001010` 則 `t = 0x00000010` 可以透過 `t = bitset & -bitset` 達到。 為了幫助理解,先把 `-bitset` 換成 `~bitset + 1`。 設 `bitset = 0x00001010` ,先考慮 `bitsey & ~bitset`: * `bitset = 0x00001010` `~bitset = 0x11110101` * 這樣算出來的值會是 `0x00000000` 若考慮 `bitsey & (~bitset + 1)` * `bitset = 0x00001010` `~bitset = 0x11110110` * * 這樣算出來的值會是 `0x00000010` 這是因為對 `~bitset` 加 1 會讓**連續的1** 進位到 `bitset` 第一個 `1` 的位置。 :::info - [x] 解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中; - [ ] 設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density; - [ ] 思考進一步的改進空間 - [ ] 閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例 ::: ## 測驗 `5` `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; } ``` `result` 用來儲存輸出的字串, `p` 用來把 `char` 填入字串中。接著排除當 `denominator ==0` 或 `numerator == 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++ = '-'; ``` 這裡把 `n` 和 `d` 強制轉成正整數,為了後面的計算方便。並且決定輸出字串的正負號。 ```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++ = '.'; ``` 這裡開始計算小數點前的數字,用 `sprintf` 把 `division` 填入輸出字串中。因為 `sprintf` 部會更改 `p` 到下一個填入的位置,因此要使用 `result + strlen(result)` 更新位置。 ```c 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 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; } ``` 這裡負責處理小數點後的輸出,並且把小數點後的數字暫時放在 `decimal` 裡,因為如我果有循環小數出現,輸出需要再循環的前後加上 `()` 。 循環的判定方法是根據 `remainder` 是否跟前面的是否有重複,因為除數固定, 被除數一樣時答案會一樣。 建立一個 `hash table` 以 `ramainder` 為 key 紀錄在 `decimal` 字串的 index。每次更新 `remainder` 時就先在 `hash table` 裡查詢是否有重複。 若找到重複的,也就是 `pos > 0` 則先把在 `decimal` 裡非循環的小數部分填入 `result` 中。因此 `PPP = pos--`。 若沒找到重複的則要新增節點到對應的 list 中。根據 `find` 可以知道 hash 的方法是 `key % size` 。 因此 `MMM = list_add` 、 `EEE = &heads[remainder % size]` 。 ` :::info - [x] 解釋上述程式碼運作原理,指出其中不足,並予以改進 - [ ] 在 Linux 核心原始程式碼的 mm/ 目錄 (即記憶體管理) 中,找到特定整數比值 (即 fraction) 和 bitwise 操作的程式碼案例,予以探討和解說其應用場景 ::: ## 測驗 `6` ```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) ``` 我們要計算 `t` 的起始位置,然後再減去參考位置,就會是 `t` 的 alignment。 因此 `M = _h` 、 `X = 0` 。 :::info - [x] 解釋上述程式碼運作原理 - [ ] 在 Linux 核心原始程式碼中找出 __alignof__ 的使用案例 2 則,並針對其場景進行解說 - [ ] 在 Linux 核心源使程式碼找出 ALIGN, ALIGN_DOWN, ALIGN_UP 等巨集,探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)。思索能否對 Linux 核心提交貢獻,儘量共用相同的巨集 ::: ## 測驗 `7` ```c 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 << KK1) << KK2; char fmt[9]; strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (KK3)], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` `is_divisble` 用來判斷是不是某個識字的倍數,以判斷 3 的倍數為例: * `uint64_t M = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1;` * `n * M <= M - 1` 可以判斷 `n` 是否為 3 的倍數。因為當 `n` 為 3 的倍數時會因為 overflow 的關係使得 `n * M = n * 1` , 小於 `M - 1`。 當 `n` 不為 3 的倍數時 必定至少有 ` UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1` , 因為不是 3 的倍數無法完整的透過 overflow 讓 `UINT64_C(0xFFFFFFFFFFFFFFFF) / 3` 歸零。 * 但是當 `n` 大於 `UINT64_C(0xFFFFFFFFFFFFFFFF) / 3` 會出現錯誤,因為根據上述的推斷可知 `n * M = n * 1` , 使得 `n * 1 <= M - 1` 不成立。在 function `is_divisible` 有個細節是 `n` 被宣告為 `uint32_t` , 因此不會有 `n > UINT64_C(0xFFFFFFFFFFFFFFFF)` / 3 的情況發生。 要根據 `div3` 和 `div5` 輸出對應的字串,相關參數如下: | div3 | div5 | start | length | | ---- | ---- | ----- | ------ | | 0 | 0 | 8 | 2 | | 1 | 0 | 0 | 4 | | 0 | 1 | 4 | 4 | | 1 | 1 | 0 | 8 | `length = (2 << KK1) << kk2` 固 `KK1 = div3` 、 `KK2 = div5` 。 從 `div3 = div5 = 1` 和 `div3 = div5 = 0` 的情況可推知 `KK3 = div3 << 2` 。