--- tags: linux2022, quiz --- # 2022q1 Homework3 (quiz3) contributed by < `freshLiver` > ## 第一題 - `GENMASK` (LEFT, RIGHT) ```c // 產生一個第 `l` 到第 `h` 個位元皆為 1、而其他位元則為 0 的數值 #define GENMASK(h, l) \ (((~0UL) >> (LEFT)) & ((~0UL) >> (l) << (RIGHT))) ``` 這段程式碼是利用對 `~0UL`(64 個 1)進行位移來將重疊部份(第 `l` 到 `h` 位元)以外的部份皆設為 0,而需要設為 0 的區段有: - 前 `63 - h` 個位元:因此 ==LEFT== 應為 `(63 - h)`。 - 後 `l` 個位元:由於已經先右移 `l` 位元了,所以要再左移 `l` 位元才能使最低 `l` 位元皆為 0,因此 ==RIGHT== 應為 `l`。 ### 比較 Linux 核心 `GENMASK` 巨集的實作,闡述其額外的考量 #### 先備知識 在看 Linux 核心中 `GENMASK` 的實作之前,要先來了解相關巨集以及 [GNU C Extensions](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 的用途以及原理: ##### 巨集 - `UL` ```c // include/uapi/linux/const.h #define __AC(X,Y) (X##Y) #define _AC(X,Y) __AC(X,Y) #define _UL(x) (_AC(x, UL)) // include/vdso/const.h #define UL(x) (_UL(x)) ``` `UL` 是一個定義在 [include/vdso/const.h](https://github.com/torvalds/linux/blob/master/include/vdso/const.h) 中的巨集,展開後會變成 `(((x##Y)))`,而其中的 `##` [token concatenation](https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html#Concatenation),可以在前處理階段時將 `x`、`y` 兩個 token 合併,因此這個巨集展該後相當於在 `x` 後加上一個 `UL` 後綴。 接著根據 [n1570 的 6.4.4.1 Integer constants](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf#%5B%7B%22num%22%3A162%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C-27%2C816%2Cnull%5D) 可以知道 `U` 以及 `L` 分別代表了 **unsigned-suffix** 以及 **long-suffix**。而在 6.4.4.1 的第 2 項則說明了這個後綴是用來指定一整數常數的型別: > 2. An integer constant begins with a digit, but has no period or exponent part. It may have a prefix that specifies its base and a **suffix that specifies its type**. 因此可以知道這個巨集是用來表示整數常數 `x` 為 64 位元無號整數([LP64](https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models))。 ##### 巨集 - `BUILD_BUG_ON_ZERO` ```c // include/linux/build_bug.h #ifdef __CHECKER__ #define BUILD_BUG_ON_ZERO(e) (0) #else #define BUILD_BUG_ON_ZERO(e) ((int)(sizeof(struct { int:(-!!(e)); }))) #endif ``` :::warning TODO : `__CHECKER__` 在哪被定義 ::: 這是一個定義在 [include/linux/build_bug.h](https://github.com/torvalds/linux/blob/master/include/linux/build_bug.h) 的巨集,可以看到當 `__CHECKER__` 有被定義時,這個巨集會展開成: ```c ((int)(sizeof(struct { int:(-!!(e)); }))) ``` 這邊使用了 Bit-Fields 的技巧, > 4. The expression that specifies the width of a bit-field shall be an integer constant expression with a **nonnegative value** that does not exceed the width of an object of the type that would be specified were the colon and expression omitted. $^{122)}$ If the value is zero, the declaration shall have no declarator. 而根據 [6.5.3.3 Unary arithmetic operators](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf#%5B%7B%22num%22%3A219%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C-27%2C816%2Cnull%5D) 的第 5 項中則有明確規範一元運算子 `!` 的結果只會是 `0` 或 `1`: > 5. The result of the logical negation operator ! is 0 if the value of its operand compares unequal to 0, **1 if the value of its operand compares equal to 0**. The result has type int. The expression !E is equivalent to (0==E). 因此當 `e` 為非 0 時,這個 bit-field 的 width 會是 `-1`,但由於 bit-field 的 width 必須為非負整數,所以會產生錯誤造成編譯失敗;相對的,當 `__CHECKER__` 沒定義或是 `e` 為 0 時則不會出現錯誤,而是會展開成常數 0 或是一個回傳 0 的表示式。 :::success 在 [bit-field](https://hackmd.io/@sysprog/c-bitfield?type=view) 一文中則有更詳細的說明。 ::: ##### Extension - `__builtin_choose_expr(const_exp, expr1, expr2)` `__builtin_choose_expr` 是 [GCC 的內建函式](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html),根據文件中對這個函式的說明: > You can use the built-in function \__builtin_choose_expr to evaluate code depending on the value of a constant expression. This built-in function returns exp1 if const_exp, which is an integer constant expression, is nonzero. Otherwise it returns exp2. > > This built-in function is analogous to the ‘? :’ operator in C, except that the expression returned has its type unaltered by promotion rules. Also, the built-in function does not evaluate the expression that is not chosen. For example, if const_exp evaluates to true, exp2 is not evaluated even if it has side effects. > > This built-in function can return an lvalue if the chosen argument is an lvalue. > > If exp1 is returned, the return type is the same as exp1’s type. Similarly, if exp2 is returned, its return type is the same as exp2. 可以知道,這個內建函式跟三元運算子 `?:;` 相似,都是根據條件是否成立來決定要進行的操作,但 `__builtin_choose_expr` 這個函式要求條件 `const_expr` 必須為常數,否則會造成編譯失敗。 :::warning TODO : 與 `?:;` 的用途、優缺點比較 ::: ##### 巨集 - `__is_constexpr` ```c #define __is_constexpr(x) \ (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8))) ``` 這個巨集被定義在 [include/linux/const.h](https://github.com/torvalds/linux/blob/master/include/linux/const.h) 中,可以用來檢查 `x` 是否能夠在**編譯時期**被視為常數。 :::warning TODO : 原理 ::: 若是 `x` 能夠在編譯階段被視為常數的話,這個巨集就會回傳 `1`,否則則會回傳 0。 #### GENMASK 實作 在 [include/linux/bits.h](https://github.com/torvalds/linux/blob/master/include/linux/bits.h) 中有定義 `GENMASK` 相關的巨集: ```c // include/linux/bits.h #define __GENMASK(h, l) \ (((~UL(0)) - (UL(1) << (l)) + 1) & \ (~UL(0) >> (BITS_PER_LONG - 1 - (h)))) #define GENMASK(h, l) \ (GENMASK_INPUT_CHECK(h, l) + __GENMASK(h, l)) ``` 而 `GENMASK` 又會根據 `__ASSEMBLY__` 的定義與否展開成不同的內容: ```c #if !defined(__ASSEMBLY__) #include <linux/build_bug.h> #define GENMASK_INPUT_CHECK(h, l) \ (BUILD_BUG_ON_ZERO(__builtin_choose_expr( \ __is_constexpr((l) > (h)), (l) > (h), 0))) #else /* * BUILD_BUG_ON_ZERO is not available in h files included from asm files, * disable the input check if that is the case. */ #define GENMASK_INPUT_CHECK(h, l) 0 #endif ``` 可以看到若是 `__ASSEMBLY__` 有被定義的話,就會展開成 ```c #define GENMASK_INPUT_CHECK(h, l) \ (BUILD_BUG_ON_ZERO(__builtin_choose_expr( \ __is_constexpr((l) > (h)), (l) > (h), 0))) ``` 而從前面的說明可以知道這段可以應該 2 種情況討論: - 若 `(l) > (h)` 能夠在編譯時期被確認是成立: 1. `__is_constexpr` 在編譯時期被相當於 1 2. `__builtin_choose_expr` 會選擇 `(l) > (h)` 的表示式 3. 由於 `(l) > (h)` 成立,所以 `BUILD_BUG_ON_ZERO` 會產生錯誤、造成編譯失敗 - 若 `(l) > (h)` 在編譯時期被確認是**不成立**,或**無法確認是否成立**: 1. `__is_constexpr` 在編譯時期被相當於 0 2. `__builtin_choose_expr` 會選擇 `0` 的表示式 3. 由於 `0` 代表不成立,所以 `BUILD_BUG_ON_ZERO` 不會產生錯誤 因此可以知道這段複雜的巨集用意是在避免使用**常數**作為 `GENMASK` 巨集的參數時誤將傳入數值比 `h` 大的 `l`。 :::warning 在 GCC 內建函式中有類似的函式 [`__builtin_constant_p(exp)`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 能夠作到與 `__is_constexpr` 相似的檢查,而原先的 `GENMASK` 巨集也是依賴這個內建函式進行檢查,但在 [2018 年 3 月時被反應](https://lore.kernel.org/lkml/42b4342b-aefc-a16a-0d43-9f9c0d63ba7a@rasmusvillemoes.dk/) `__builtin_constant_p` 在編譯時可能無法在正確的時間點檢查 `exp`,因此會造成 `__builtin_choose_expr` 的 `const_expr` 部份被認為是非常數而導致編譯失敗。直到在 [2021 年 3 月時才改用 `__is_constexpr`](https://github.com/torvalds/linux/commit/f747e6667ebb2ffb8133486c9cd19800d72b0d98) 來取代 `__builtin_constant_p` 進行判斷。 ::: 而在檢查完 `h`、`l` 之後(或因 `__ASSEMBLY__` 未定義而不進行檢查),就會接著產生 mask 的部份: ```c #define __GENMASK(H, L) \ (((~UL(0)) - (UL(1) << (L)) + 1) & \ (~UL(0) >> (BITS_PER_LONG - 1 - (H)))) ``` :::info 因為 `1` 與 `l` 的字型非常相似,所以為了愛護眼睛,我將巨集的 `h` 以及 `l` 改用大寫字母 `H` 以及 `L` 替代。 ::: 可以看到這個巨集一樣是對 64 位元的無號整數 0 取一補數來產生 64 個 1 進行位移,而這邊將這個巨集分成兩部份討論: - `((~UL(0)) - (UL(1) << (L)) + 1)` 的部份: - `(~UL(0))`:產生 64 個 1 - `- (UL(1) << (L))`:將第 `L` 個位元值設成 0 - `+ 1`:使用 `+ 1` **進位的特性**,將最低位的 0(也就是第 `L` 個位元)設成 1,而其它更低的位元(即第 `0` 至 `L-1` 位元)則會因進位被設成 0 - 到這邊可以產生==第 `L-1` ~ 0 位元皆為 0 的 mask== - `(~UL(0) >> (BITS_PER_LONG - 1 - (H)))` 的部份: - `(~UL(0)`:產生 64 個 1 - `(BITS_PER_LONG - 1 - (H))`:由於位元的編號是從 0 開始,所以最高位應為 63(即 `BITS_PER_LONG - 1`),而 `- (H)` 則是找出應要右移的位數 - `>> (BITS_PER_LONG - 1 - (H))`:右移讓最高的 `63 - (H)` 個位元為 0 - 到這邊可以產生==第 `63` ~ `H+1` 位元皆為 0 的 mask== 最後再將兩個 mask 進行 AND 運算即可產生第 `H` ~ `L` 位元皆為 1 的 mask。 ### Linux 核心原始程式碼中二處 GENMASK 巨集和 include/linux/bitfield.h 的應用案例 --- ## 第二題 - `ALIGN_DOWN` (EXP1) ```c struct foo; struct fd { struct foo *foo; unsigned int flags; }; enum { FOO_DEFAULT = 0, FOO_ACTION, FOO_UNLOCK, } FOO_FLAGS; static inline struct fd to_fd(unsigned long v) { return (struct fd){EXP1, v & 3}; } ``` 這段程式碼是要將一地址 `v` 看作是結構體 `foo` 的起始位置,若地址 `v` 沒有以 4 位元組進行對齊的話,則需要將地址向下對齊,因此 EXP1 要先捨去地址的後 2 個位元,然後再轉型成結構體 `foo` 的指標,即 ==EXP1== 應為 `(struct foo *)(v & ~3)`。 ### 在 Linux 核心原始程式碼找出類似技巧的實作 (例如檔案系統) 並解說 在[第九題的延伸問題](#%E6%89%BE%E5%87%BA%E4%B8%A6%E8%AA%AA%E6%98%8E-Linux-%E6%A0%B8%E5%BF%83%E4%B8%AD-round-upround-down-%E7%9A%84%E6%87%89%E7%94%A8%E5%A0%B4%E5%90%88)中一併討論。 --- ## 第三題 - [LeetCode 190. Reverse Bits](https://leetcode.com/problems/reverse-bits/) (EXP2, EXP3) ```c= #include <stdint.h> uint8_t rev8(uint8_t x) { x = (x >> 4) | (x << 4); // 以 4 位元為單位,兩段互換 x = ((x & 0xCC) >> 2) | (EXP2); // 以 2 位元為單位,0,1 2,3 段兩兩互換 x = ((x & 0xAA) >> 1) | (EXP3); // 以 1 位元為單位,0,1 2,3 4,5 6,7 段兩兩互換 return x; } ``` 將 $n$ 個位元以不同單位大小分割成偶數段,每次將奇數部份以及偶數部份透過位移以及 bitwise-or 互換,而因為 `0xCC` 與 `0xAA` 分別代表 `0b11001100` 與 `0x10101010`,即分別為單位大小為 2 位元及 1 位元的偶數段部份,所以 EXP2 與 EXP3 應為對應單位大小的奇數段部份。因此 ==EXP2== 應為 `0x33` (`0b00110011`),而 ==EXP3== 則應為 `0x55` (`0b01010101`) ### 在 Linux 核心原始程式碼找出類似技巧的實作並解說其應用場景 在 [include/linux/bitrev.h](https://github.com/torvalds/linux/blob/master/include/linux/bitrev.h) 中包含了 1, 2, 4 位元組反轉的巨集實作,可以看到大致上分成 3 種實作方式: 1. bitwise 反轉(常數) 當要反轉的值屬於常數時,則會直接使用與測驗題相似的方式,應該是為了讓編譯器能夠將常數運算直接最佳化掉。 2. 查表(非常數且無硬體指令) 在 [lib/bitrev.c](https://github.com/torvalds/linux/blob/master/lib/bitrev.c) 中有透過陣列 `byte_rev_table` 列舉了 8 位元對應的反轉結果,因此當 `CONFIG_HAVE_ARCH_BITREVERSE` 沒有被定義時,可以不用以 4, 2, 1 位元作為單位大小進行反轉,直接透過查表找到該位元組對應的反轉結果即可。 3. 反轉指令(非常數但有硬體指令) 在某些架構的指令集中可能有能夠反轉位元的指令,像是 [ARM 的 `RBIT` 指令](https://developer.arm.com/documentation/dui0489/h/arm-and-thumb-instructions/rev--rev16--revsh--and-rbit) 就能夠直接反轉一個 32 位元的資料。因此在 `CONFIG_HAVE_ARCH_BITREVERSE` 有被定義時就能夠直接使用指令進行更有效率的反轉操作。 --- ## 第四題 - foreach (EXP4, EXP5) ```c #include <assert.h> #define _foreach_no_nullval(i, p, arr) \ assert((i) >= sizeof(arr) / sizeof(arr[0]) || (p)) #define foreach_int(i, ...) \ for (unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0); \ _foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int); \ (i) = ((int[]){__VA_ARGS__, 0})[EXP4]) #define foreach_ptr(i, ...) \ for (unsigned _foreach_i = \ (((i) = (void *) ((typeof(i)[]){__VA_ARGS__})[0]), 0); \ (i); (i) = (void *) ((typeof(i)[]){__VA_ARGS__, NULL})[EXP5], \ _foreach_no_nullval(_foreach_i, i, \ ((const void *[]){__VA_ARGS__}))) ``` 兩個 `for each` 巨集的實作概念都差不多,使用了 [Variadic Macro](https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html) 進行陣列宣告,並透過 `for` 迴圈以及 `,` 運算子: > 在規格書中的 [6.5.17 Comma operator](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf#%5B%7B%22num%22%3A270%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C0%2C792%2C0%5D) 中關於 `,` 這個運算子的說明: > 1. Syntax : > ``` > expression: > assignment-expression > expression , assignment-expression > ``` > 2. The left operand of a comma operator is evaluated as a void expression; there is a sequence point between its evaluation and that of the right operand. Then the right operand is evaluated; the result has its type and value. > 3. **EXAMPLE** As indicated by the syntax, the comma operator (as described in this subclause) cannot appear in contexts where a comma is used to separate items in a list (such as arguments to functions or lists of initializers). On the other hand, it can be used within a parenthesized expression or within the second expression of a conditional operator in such contexts. In the function call `f(a, (t=3, t+2), c)` the function has three arguments, the second of which has the value 5. 因此在 `for` 迴圈的初始化部份會先執行 `,` 左側,也就是先將陣列的第一個元素賦值給變數 `i`,最後才會將 `,` 右側的 `0`,也就是陣列的初始 index 賦值給 `_foreach_i`。 除了初始化用來儲存陣列的變數 `i` 以及陣列 index 的變數 `_foreach_i` 之外,每次迴圈結束還需要增加 `_foreach_i` 並將 `i` 移動到下一個元素,因此 `_foreach_i` 可以直接透過 `++_foreach_i` 或 `_foreach_i++` 增加,但 `i` 由於並不是指標,所以必須重新初始化陣列,並將下一個元素透過 `_foreach_i` 賦值給 `i`,因此 ==EXP4== 與 ==EXP5== 皆應為 `++_foreach_i` 才能在每次迭代結束時將正確的元素重新賦值給 `i`。 ### 在 Linux 核心原始程式碼找出類似技巧的實作並解說其應用場景 透過 `$ grep -rP --include="*.[ch]" "\\[\\].*__VA_ARGS__.*"` 尋找相關用法大多是用來初始化陣列或是檢查巨集參數的數量,其中與這題比較相近的是: ```c // drivers/gpu/drm/i915/i915_reg.h /* * Given the arbitrary numbers in varargs, pick the 0-based __index'th number. * * Always prefer _PICK_EVEN() over this if the numbers are evenly spaced. */ #define _PICK(__index, ...) (((const u32 []){ __VA_ARGS__ })[__index]) ``` 除了透過單純宣告成一個陣列之外,還能透過指定 index 來取得目標參數。 --- ## 第五題 - [LeetCode 29. Divide Two Integers](https://leetcode.com/problems/divide-two-integers/) ```c= #include <limits.h> int divide(int dividend, int divisor) { int signal = 1; unsigned int dvd = dividend; if (dividend < 0) { signal *= -1; dvd = ~dvd + 1; } unsigned int dvs = divisor; if (divisor < 0) { signal *= -1; dvs = ~dvs + 1; } int shift = 0; while (dvd > (EXP6)) shift++; unsigned int res = 0; while (dvd >= dvs) { while (dvd < (dvs << shift)) shift--; res |= (unsigned int) 1 << shift; EXP7; } if (signal == 1 && res >= INT_MAX) return INT_MAX; return res * signal; } ``` ### 指出可改進部份並實作 無乘法、無迴圈以外的 branch ```c int divide(int dividend, int divisor) { uint32_t sign = 0; int dvd_mask = dividend >> 31; sign ^= dvd_mask & 0xFFFFFFFF; // explicitly cast to unsigned to avoid overflow uint32_t dvd = (uint32_t) (dividend ^ dvd_mask) - dvd_mask; int dvs_mask = divisor >> 31; sign ^= dvs_mask & 0xFFFFFFFF; // explicitly cast to unsigned to avoid overflow uint32_t dvs = (uint32_t) (divisor ^ dvs_mask) - dvs_mask; uint8_t shift = 0; while (dvd > (dvs << shift)) ++shift; uint32_t res = 0; while (dvd >= dvs) { while (dvd < (dvs << shift)) --shift; // 1 should be unsigned to avoid overflow (1 << 31) res |= 1U << shift; dvd -= dvs << shift; } res |= -!!(res > 0x7FFFFFFFU && !sign) & 0x7FFFFFFFU; // explicitly cast to unsigned to avoid overflow res ^= (uint32_t)(res > 0x7FFFFFFFU && !sign) << 31; return (res ^ sign) - sign; } ``` ### 找出並討論 Linux 核心原始程式碼中找出針對整數/浮點數/定點數除法特別撰寫的程式碼 --- ## 第六題 - [LeetCode 149. Max Points on a Line](https://leetcode.com/problems/max-points-on-a-line/) ```c= #include <stdbool.h> #include "list.h" struct Point { int x, y; }; struct point_node { int p1, p2; struct list_head link; }; static bool can_insert(struct list_head *head, int p1, int p2) { struct point_node *pn; list_for_each_entry (pn, head, link) return EXP8; return true; } static int gcd(int x, int y) { while (y) { int tmp = y; y = x % y; x = tmp; } return x; } static int maxPoints(struct Point *points, int pointsSize) { if (pointsSize <= 2) return pointsSize; int i, j, slope_size = pointsSize * pointsSize / 2 + 133; int *dup_cnts = malloc(pointsSize * sizeof(int)); int *hori_cnts = malloc(pointsSize * sizeof(int)); int *vert_cnts = malloc(pointsSize * sizeof(int)); int *slope_cnts = malloc(slope_size * sizeof(int)); memset(hori_cnts, 0, pointsSize * sizeof(int)); memset(vert_cnts, 0, pointsSize * sizeof(int)); memset(slope_cnts, 0, slope_size * sizeof(int)); for (i = 0; i < pointsSize; i++) dup_cnts[i] = 1; struct list_head *heads = malloc(slope_size * sizeof(*heads)); for (i = 0; i < slope_size; i++) INIT_LIST_HEAD(&heads[i]); for (i = 0; i < pointsSize; i++) { for (j = i + 1; j < pointsSize; j++) { if (points[i].x == points[j].x) hori_cnts[i]++, hori_cnts[j]++; if (points[i].y == points[j].y) vert_cnts[i]++, vert_cnts[j]++; if (points[i].x == points[j].x && points[i].y == points[j].y) dup_cnts[i]++, dup_cnts[j]++; if (points[i].x != points[j].x && points[i].y != points[j].y) { int dx = points[j].x - points[i].x; int dy = points[j].y - points[i].y; int tmp = gcd(dx, dy); dx /= tmp; dy /= tmp; int hash = dx * dy - 1333 * (dx + dy); if (hash < 0) hash = -hash; hash %= slope_size; if (can_insert(&heads[hash], i, j)) { struct point_node *pn = malloc(sizeof(*pn)); pn->p1 = i; pn->p2 = j; EXP9; } } } } for (i = 0; i < slope_size; i++) { int index = -1; struct point_node *pn; list_for_each_entry (pn, &heads[i], link) { index = pn->p1; slope_cnts[i]++; } if (index >= 0) slope_cnts[i] += dup_cnts[index]; } int max_num = 0; for (i = 0; i < pointsSize; i++) { if (hori_cnts[i] + 1 > max_num) max_num = hori_cnts[i] + 1; if (vert_cnts[i] + 1 > max_num) max_num = vert_cnts[i] + 1; } for (i = 0; i < slope_size; i++) { if (slope_cnts[i] > max_num) max_num = slope_cnts[i]; } return max_num; } ``` ### 指出可改進部份並實作 ### 擴充 LeetCode 題目,考慮更多座標點的輸入 (例如超過 10 萬個) 時,設計有效的資料結構以降低記憶體開銷,並確保快速的執行 --- ## 第七題 - `ilog32` ```c /** * @brief 對於一 32 位元無號整數 @v 最少需要幾位元才能儲存 * * 儲存最少需要的位元數 = 最高位元 1 的位置 (32-clz) * 因此可以可以透過 BITMASK 將資料分成高位元以及低位元兩部份,並檢查最高位元的 1 * 在哪部份: * - 若是不在高位元部份就縮小範圍再檢查一次。 * - 若是在高位元部份的話就紀錄並移除低位元部份,然後縮小範圍再重新檢查一次。 */ int ilog32(uint32_t v) { int ret = v > 0; int m = (v > 0xFFFFU) << 4; v >>= m; ret |= m; m = (v > 0xFFU) << 3; v >>= m; ret |= m; m = (v > 0xFU) << 2; v >>= m; ret |= m; m = EXP10; v >>= m; ret |= m; EXP11; return ret; } ``` 以二元搜尋的概念找出最高位元的 1 的位置,從中間開始,每次都將檢查範圍縮小一半,當最高位 1 位於高位元部份時,則可以右移掉低位元部份,讓兩種狀況能以相同的形式繼續進行檢查。若是以這個邏輯實作的話,只要重複三行程式碼: ```c= // 檢查最高位 1 是否在 16 - 31 位元 int m = (v > 0xFFFFU) << 4; v >>= m; // 若是的話 m 會變成 16,否則為 0 (代表能夠捨棄的位元數) ret |= m; // 相當於 ret += m,即用來紀錄最高位 1 的位置 // 檢查最高位 1 是否在 或 8 - 31 位元 m = (v > 0xFFU) << 3; v >>= m; // 若是的話 m 為 8,否則為 0 (代表能夠捨棄的位元數) ret |= m; // 檢查最高位 1 是否在 或 4 - 31 位元 m = (v > 0xFU) << 2; v >>= m; // 若是的話 m 為 4,否則為 0 (代表能夠捨棄的位元數) ret |= m; // 檢查最高位 1 是否在 或 2 - 31 位元 m = (v > 0x3U) << 1; /* EXP10 */ v >>= m; // 若是的話 m 為 2,否則為 0 (代表能夠捨棄的位元數) ret |= m; // 檢查最高位 1 是否在 或 1 - 31 位元 m = (v > 0x1U) << 0; v >>= m; // 若是的話 m 為 1,否則為 0 (代表能夠捨棄的位元數) ret |= m; ``` 可以簡單的知道 ==EXP10== 應為 `(v > 0x3U) << 1`,即用來檢查最高位元 1 是否在第 2 至 31 位元。 但這樣會因為每次都分成兩長度不為 0 的部份,所以最多只能檢查 31 個位元,造成當 `v == 1` 時會產生錯誤結果,因此最後還需要檢查 `v` 是否在第 0 個位元,即 `v` 是否為 0: ```c=26 ret += (v > 0); ``` 這邊要注意的是,由於前面都是透過 `|` 設定 `ret` 的第 0 至第 5 位元來紀錄所需位元數,因此最後需要改用 `+`,才不會在某些情況(第 23 行條件成立時)產生錯誤結果。 在了解檢查機制之後,需要注意到在原程式碼的一開始宣告 `int ret = v > 0;` 時就已經先檢查 `v > 0` 的情況了,所以需要改用 `+` 的是第 23 至 25 行的部份: ```c=22 // 檢查最高位 1 是否在 或 1 - 31 位元 m = (v > 0x1U) << 0; v >>= m; // 若是的話 m 為 1,否則為 0 (代表能夠捨棄的位元數) ret += m; ``` 而這邊又可以在進行簡化(左移 `0` 可以忽略、`v` 不須再右移、`m` 與 `ret` 的操作可以合併): ```c=23 ret += (v > 0x1); ``` 應此 ==EXP11== 最精簡的表示法應為 `ret += v > 1`。 ### 在 Linux 核心原始程式碼找出類似實作並解說其應用場景 [quiz4 的 ilog2 說明](https://hackmd.io/@freshLiver/linux2022q1-hw4-quiz#ilog2-巨集)。 ### 缺乏硬體 ctz/clz 指令的微處理器上,如何實作 branchless 的 ilog 研讀論文[《Using de Bruijn Sequences to Index a 1 in a Computer Word》](http://supertech.csail.mit.edu/papers/debruijn.pdf) ### 實作編譯時期 (compile-time) 的 ilog2 實作 運用〈[你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor)〉和〈[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics)〉提及的技巧 --- ## 第八題 - 二元搜尋樹 (C++) ```cpp= typedef struct tnode *tree; struct tnode { int data; tnode *left; tnode *right; tnode(int d) { data = d; left = right = 0; } }; ``` ```cpp= // native.cpp void remove_data(tree &t, int d) { tnode *p = t; tnode *q; while (p != 0 && p->data != d) { if (d < p->data) { q = p; p = p->left; } else { q = p; p = p->right; } } if (p != 0) { if (p->left == 0) if (p == t) t = p->right; else if (p == q->left) q->left = p->right; else q->right = p->right; else if (p->right == 0) if (p == t) t = p->left; else if (p == q->left) q->left = p->left; else q->right = p->left; else { tnode *r = p->right; while (r->left) { q = r; r = r->left; } p->data = r->data; if (r == p->right) p->right = r->right; else q->left = r->right; p = r; } delete p; } } ``` 使用 indirect pointer 改寫簡化: ```cpp= // improve.cpp void remove_data(tree &t, int d) { tnode **p = &t; while (*p != 0 && (*p)->data != d) { if (d < (*p)->data) EXP12; else EXP13; } tnode *q = *p; if (!q) return; if (!q->left) *p = q->right; else if (!q->right) *p = q->left; else { tnode **r = &q->right; while ((*r)->left) r = EXP14; q->data = (*r)->data; q = *r; *r = q->right; } delete q; } ``` ### 以更精簡、高效且符合 C99 標準進行實作探討效率的提升 ### 探討針對記憶體佈局 (memory layout) 的改進策略 研讀 [Static B-Trees](https://en.algorithmica.org/hpc/data-structures/s-tree/) --- ## 第九題 - ALIGN_UP ```c /* maximum alignment needed for any type on this platform, rounded up to a power of two */ #define MAX_ALIGNMENT 16 /* Given a size, round up to the next multiple of sizeof(void *) */ #define ROUND_UP_TO_ALIGNMENT_SIZE(x) \ (((x) + MAX_ALIGNMENT - MMM) & ~(NNN)) ``` 與實作四捨五入的概念(加上 `0.5` 再捨去小數點部份)有點相似,直接對地址加上對齊大小(這段程式碼為 16 位元組對齊),接著再捨去地址的最低 4 位元,因此 `~(NNN)` 應為除最低 4 位元皆為 1 的數,即 ==NNN== 應為 `MAX_ALIGNMENT - 1` (15)。 但是直接加上對齊大小會讓已對齊的地址也被向上對齊,因此要先讓原地址 `x` 減 1 才能確保已對齊的地址向上對齊後保持不變,所以 ==MMM== 應為 1。 ### 撰寫出對應 ROUND_DOWN 巨集 ```c #define MAX_ALIGNMENT 16 #define ROUND_DOWN_TO_ALIGNMENT_SIZE(x) ((x) & ~(MAX_ALIGNMENT - 1)) ``` 跟第二題的概念相同,直接捨棄地址最後的 $log_2(MAX\_ALIGNMENT)-1$ 個位元。 ### 找出並說明 Linux 核心中 round-up/round-down 的應用場合 md raid metadata ```c // driver/md/raid1.h /* * each barrier unit size is 64MB fow now * note: it must be larger than RESYNC_DEPTH */ #define BARRIER_UNIT_SECTOR_BITS 17 #define BARRIER_UNIT_SECTOR_SIZE (1<<17) // driver/md/raid1.c static sector_t align_to_barrier_unit_end(sector_t start_sector, sector_t sectors) { sector_t len; WARN_ON(sectors == 0); /* * len is the number of sectors from start_sector to end of the * barrier unit which start_sector belongs to. */ len = round_up(start_sector + 1, BARRIER_UNIT_SECTOR_SIZE) - start_sector; if (len > sectors) len = sectors; return len; } ``` --- ## 第十題 - DIVIDE_ROUND_CLOSEST ```c #define DIVIDE_ROUND_CLOSEST(x, divisor) \ ({ \ /* evaluate once */; \ typeof(x) __x = x; \ typeof(divisor) __d = divisor; \ /* check unsigned or not */; \ (((typeof(x)) -1) > 0 || ((typeof(divisor)) -1) > 0 || \ (((__x) > 0) == ((__d) > 0))) \ ? ((7777 /* RRR */) / (__d)) \ : ((8888 /* SSS */) / (__d)); \ }) /** * True : SSS 皆可以用 "被除數 + 0.5 * 除數" 表示 * - TTT 兩數皆為無號 * 1. 皆為正數 : SSS = 被除數 + 0.5 * 除數 * 2. 皆非 0 : divide by zero,不處理 * * - FTT 被除數為有號,除數為無號 * 1. 皆為正數 : SSS = 被除數 + 0.5 * 除數 * 2. 除數必為 0 : divide by zero,不處理 * * - TFT 被除數為無號,除數為有號 * 1. 皆為正數 : SSS = 被除數 + 0.5 * 除數 * 2. 被除數為 0,除數為負數 : 0 >= SSS > 除數 * 3. 被除數為 0,除數為 0 : divide by zero,不處理 * * - FFT 兩數皆為有號數 * 1. 皆為正數 : SSS = 被除數 + 0.5 * 除數 * 2. 皆為負數 : SSS = 被除數 - 0.5 * |除數| * 3. 被除數為 0,除數為負數 : 0 >= SSS > 除數 * 4. 被除數為負數,除數為 0 : divide by zero,不處理 * 5. 被除數為 0,除數為 0 : divide by zero,不處理 * * - FTF 被除數為無號,除數為有號,恰一數為非正數 * 1. 被除數為 0,除數為正 : -除數 < SSS < 除數 * 2. 被除數為正,除數為負 : SSS = 被除數 + 0.5 * |除數| * 3. 被除數為正,除數為 0 : divide by zero,不處理 * * - TFF 被除數為有號,除數為無號,恰一數為非正數 * 1. 被除數為 0,除數為正 : -除數 < SSS < 除數 * 1. 被除數為負,除數為正 : * 1. 被除數為正,除數為 0 : divide by zero,不處理 * * - TTF 兩數皆為無號數,但恰有一數為非正數 (0) * 1. 被除數為 0,除數為正數 : -除數 < SSS < 除數 * 2. 被除數為正數,除數為 0 : divide by zero,不處理 * * * False : RRR 皆可以用 "被除數 - 0.5 * 除數" 表示 * - FFF 兩數皆為有號型別,且恰有一數為非正數 : * 1. 被除數為正,除數為負 : RRR = 被除數 + 0.5 * |除數| * 2. 被除數為負,除數為正 : RRR = 被除數 - 0.5 * 除數 * 3. 被除數為 0,除數為正 : -除數 < RRR < 除數 * 4. 被除數為正,除數為 0 : divide by zero,不處理 * */ ``` ### 找出並說明 Linux 核心中 div round-up/closest 的實作與使用情景 在 `insmod` 時,會使用到定義在 `include/linux/module_decompress.c` 中的 [`module_decompress`](https://hackmd.io/rJ7YiwC_TSa6C5l2i-p0kg?both#%E5%82%B3%E9%81%9E%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84%E5%8F%83%E6%95%B8),而其中又會使用到 `DIV_ROUND_UP` 來確保核心模組解壓縮後仍有足夠空間能儲存資訊。 ## 第十一題 - Integer Square Root ```c= static inline unsigned long fls(unsigned long word) { int num = 64 - 1; if (!(word & (~0ul << 32))) { num -= 32; word <<= 32; } if (!(word & (~0ul << (64 - 16)))) { num -= 16; word <<= 16; } if (!(word & (~0ul << (64 - 8)))) { num -= 8; word <<= 8; } if (!(word & (~0ul << (64 - 4)))) { num -= 4; word <<= 4; } if (!(word & (~0ul << (64 - 2)))) { num -= 2; word <<= 2; } if (!(word & (~0ul << (64 - 1)))) num -= 1; return num; } ``` ```c= unsigned long i_sqrt(unsigned long x) { unsigned long b, m, y = 0; if (x <= 1) return x; m = 1UL << (fls(x) & ~1UL); while (m) { b = y + m; XXX; if (x >= b) { YYY; y += m; } ZZZ; } return y; } ``` ### 嘗試利用硬體的 clz/ctz 指令改寫 ### 在 Linux 核心找出類似的巨集和程式碼,說明其應用場景