--- tags: linux2022 --- # 2022q1 Homework2 (quiz2) contributed by < [howardjchen](https://github.com/howardjchen) > ### 測驗 1 考慮以下對二個無號整數取平均值的程式碼: ```c #include <stdint.h> uint32_t average(uint32_t low, uint32_t high) { return low + (high - low) / 2; } ``` 接著我們可改寫為以下等價的實作: ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (EXP1); } ``` - **想法&思路** ```(a >> 1) + (b >> 1)``` 可以看成 a/2 + b/2 但不等於 (a+b)/2 因為會涉及到進位的問題 如果 a 或 b 有兩個都是奇數的話, a/2 + b/2 跟 (a+b)/2 就會相差 1 因此 EXP1 的作用是如果 a 和 b 都是奇數的時候需要加 1 ``` EXP1 = a & b & 1 ``` 所以可以改寫成 ```c #include <stdint.h> uint32_t average(uint32_t a, uint32_t b) { return (a >> 1) + (b >> 1) + (a & b & 1); } ``` 我們再次改寫為以下等價的實作: ```c uint32_t average(uint32_t a, uint32_t b) { return (EXP2) + ((EXP3) >> 1); } ``` - **想法&思路** 這個直觀上很像 ```return low + (high - low) / 2;``` 因此```EXP2 = low``` ```EXP3 = high - low``` 但是並不知道誰是 low 跟 high 另一個想法是從[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics) 有提到為了避免 overflow - ```(x + y) / 2``` 這樣的運算,有個致命問題在於 (x + y) 可能會導致 overflow (考慮到 x 和 y 都接近 [UINT32_MAX](https://msdn.microsoft.com/en-us/library/mt764276.aspx),亦即 32-bit 表示範圍的上限之際) * 於是我們可以改寫為以下: ```cpp (x & y) + ((x ^ y) >> 1) ``` * 用加法器來思考: `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)` 因此 ``` EXP2 = a & b EXP3 = a ^ b ``` ```c uint32_t average(uint32_t a, uint32_t b) { return (a & b) + ((a ^ b) >> 1); } ``` > 2. 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章) 我們可以透過下列指令做編譯最佳化並輸出處和語言 ```test.s``` ```shell gcc -S -O3 test.c ``` **(high + low) / 2;** ```c= endbr64 leal (%rsi,%rdi), %eax shrl %eax ret ``` [What does the LEAL assembly instruction do?](https://stackoverflow.com/questions/11212444/what-does-the-leal-assembly-instruction-do) > LEA (load effective address) just computes the address of the operand, it does not actually dereference it. Most of the time, it's just doing a calculation like a combined multiply-and-add for, say, array indexing. - 簡單來說 leal 可以看成相加, 所以把兩個變數相加後放在 ```%eax``` - shrl 是 shift left, 把```%eax``` 除 2 **low + (high - low) / 2;** ```c= endbr64 movl %esi, %eax subl %edi, %eax shrl %eax addl %edi, %eax ret ``` - 前兩行就是相減 - 第四行向右移 1 - 最後加上 low **(a >> 1) + (b >> 1) + (a & b & 1);** ```c= endbr64 movl %edi, %eax movl %esi, %edx andl %esi, %edi // a & b shrl %eax // a >> 1 shrl %edx // b >> 1 andl $1, %edi // a & b & 1 addl %edx, %eax // (a >> 1) + (a & b & 1) addl %edi, %eax // (a >> 1) + (b >> 1) + (a & b & 1) ret ``` **(a & b) + ((a ^ b) >> 1);** ```c= endbr64 movl %edi, %eax andl %esi, %edi // a & b xorl %esi, %eax // a ^ b shrl %eax // (a ^ b) >> 1 addl %edi, %eax // (a & b) + ((a ^ b) >> 1) ret ``` > 研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用 ### 測驗 2 改寫 [解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation) 一文的「不需要分支的設計」一節提供的程式碼 min ```c= int32_t min(int32_t a, int32_t b) { int32_t diff = (a - b); return b + (diff & (diff >> 31)); } ``` 針對 max 的情形我們可以改寫成 ```c= int32_t max(int32_t a, int32_t b) { int32_t diff = (a - b); return a - (diff & (diff >> 31)); } ``` 若要更近一步改寫成題目的形式 ```c= #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((EXP4) & -(EXP5)); } ``` 所以 EXP4 = a ^ b - if a > b, then return a 那就表示 ```((a ^ b) & -(EXP5)) = 0``` -(EXP5) 需要等於 0 也就是 EXP5 = 0 - if a <= b, then return b 那就表示 ```((a ^ b) & -(EXP5)) = a ^ b``` -(EXP5) 需要等於 0xffffffff'h = -1'd 也就是 EXP5 = 1 所以 EXP5 = -((a - b) >> 31) 因此我們可以重新把這個 function 寫成 ```c= #include <stdint.h> uint32_t max(uint32_t a, uint32_t b) { return a ^ ((a ^ b) & ((a - b) >> 31)); } ``` - 延伸問題: 2. 針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作 不管是有號整數或是無號整數, (a-b) 都是 -1,所以實作一樣 :::success 4. Linux 核心也有若干 branchless / branch-free 的實作,例如 lib/sort.c: ```c /* * Logically, we're doing "if (i & lsbit) i -= size;", but since the * branch is unpredictable, it's done with a bit of clever branch-free * code instead. */ __attribute_const__ __always_inline static size_t parent(size_t i, unsigned int lsbit, size_t size) { i -= size; i -= size & -(i & lsbit); return i / 2; } ``` 請在 Linux 核心原始程式碼中,找出更多類似的實作手法。請善用 git log 檢索。 ::: ### 測驗 3 考慮以下 64 位元 GCD (greatest common divisor, 最大公因數) 求值函式: ```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; } ``` 改寫為以下等價實作: ```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; } ``` > 1. if both x, y are 0, $gcd(0,0) = 0$ > 2. $gcd(x,0)=x$ and $gcd(0,y)=y$ ```c if (!u || !v) return u | v; ``` > 3. If x and y are both even, $gcd(x,y) = 2 * gcd(\dfrac{x}{2}, \dfrac{y}{2})$ ```c for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` 所以$gcd(u, v) = 2^n * gcd(\dfrac{u}{2}, \dfrac{v}{2})$ $n = shift$ > 4. If x is even and y is odd, $gcd(x,y) = 2 * gcd(\dfrac{x}{2}, y)$ > 5. If x is odd and y is even, then $gcd(x,y) = 2 * gcd(x, \dfrac{y}{2})$ ```c while (!(u & 1)) u /= 2; ``` ```c while (!(v & 1)) v /= 2; ``` 這兩個表示 u 跟 v 不會同時是偶數 是利用輾轉相除法的原理,只是用了連續減法來找出相除的餘數結果。 輾轉相除法就是除到餘數為 0 就是答案。 最後一步 ```u = v``` 的時候, 就是餘數為 0 的時候,此時 ```uint64_t t = u - v```會讓 t 為 0, 然後被指派給 v. 所以 COND 就是餘數 v , 而 u 在這裡就是最後的那個最大公因數。 但要記得真正的答案是 $2^n * gcd(\dfrac{u}{2}, \dfrac{v}{2})$ $n = shift$ 因此 RET 就是 u << shift. > 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升; __builtin_ctz 並不是 C 的標準函式庫,只有在 gcc 中可以使用 :::success int __builtin_ctz (unsigned int x) int __builtin_ctzl (unsigned long) int __builtin_ctzll (unsigned long long) 返回右起第一個1之後的0的個數 Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined. ::: 似乎可以改善 ```c int shift; for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` 成為 ```c int shift_u = __builtin_ctz(u); int shift_v = __builtin_ctz(v); int shift = (shift_u > shift_v)? shift_v : shift_u; ``` 另一方面還有 ```c while (!(u & 1)) u /= 2; ``` 成為 ```c u = u >> __builtin_ctz(u); ``` 因此可以改寫程式碼如下: ```c #include <stdint.h> uint64_t gcd64(uint64_t u, uint64_t v) { if (!u || !v) return u | v; int shift_u = __builtin_ctz(u); int shift_v = __builtin_ctz(v); int shift = (shift_u > shift_v)? shift_v : shift_u; u >>= shift_u; do { v >>= _shift_v; if (u < v) { v -= u; } else { uint64_t t = u - v; u = v; v = t; } } while (v); return (u << shift); } ``` 各執行了 100000 次得到結果,使用```__builtin_ctz```可以提升約 1.49 倍的效能 ```c size_t rand64() { time_t t; srand((unsigned) time(&t)); return (rand() % 0xffffffff); } float execute_cost(size_t (*func)(size_t, size_t), int times) { clock_t start = clock(); int cnt = 0; while (times--) { size_t ans = func(testbench[cnt], testbench[cnt+1]); cnt++; } clock_t end = clock(); return (float)(end - start) / CLOCKS_PER_SEC; } ``` ```bash gcd64 cost: 0.000961 gcd64_f cost: 0.000641 ``` > Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景