# 2023q1 Homework2 (quiz2) contributed by <`bonianlee`> ### 測驗 `1` 參照 [你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise#%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-bitwise-%E6%93%8D%E4%BD%9C),考慮 next_pow2 可針對給定無號 64 位元數值 x,找出最接近且大於等於 2 的冪的值,例如: - `next_pow2(7)` = 8 - `next_pow2(13)` = 16 - `next_pow2(42)` = 64 測驗題題目中提到的一種實作方式為: ```c #include <stdint.h> static inline uint64_t pow2(uint8_t e) { return ((uint64_t)1) << e; } uint64_t next_pow2(uint64_t x) { uint8_t lo = 0, hi = 63; while (lo < hi) { uint8_t test = (lo + hi)/2; if (x < pow2(test)) { hi = test; } else if (pow2(test) < x) { lo = test+1; } else { return pow2(test); } } return pow2(lo); } ``` 它採用的方法是先取 `uint64_t` 的最小與最大的 2 的冪的值,分別以次冪數形式儲存在 `lo` 跟 `hi` 中,也就是 $2^{lo} = 2^0$ 與 $2^{hi} = 2^{63}$ ,並且將 `x` 與 `test = (lo + hi)/2` 次冪數的值 $2^{test}$ 做比較,若 `x` 比較小,則將上限次冪 `hi` 縮小成 `test` 的值,反之,若 `x` 比較大,則將下限次冪 `lo` 增加到 `test + 1` 的值,最後的回傳結果有兩種 1. `return pow2(test)` 當 `x` 剛好等於 $2^{test}$ 時,則直接回傳 $2^{test}$ 2. `return pow2(lo)` 當 `x` 並不是 2 的冪的值,則回傳最接近且大於 `x` 的 2 的冪的值,即 $2^{lo}$ 而題目提到另一種使用位元運算的實作分支,它所做的事情是以擁有最大位元的 `1` 為界,向後「填補」位元為 `0` 的位元 ```c x = 0010000000000000 x = 0011000000000000 x = 0011110000000000 x = 0011111111000000 x = 0011111111111111 ``` 當「填補」的動作完成後,為了符合預期,找出最接近且大於等於 2 的冪的值,可以進行以下操作 ```c return x + 1; ``` 則可以得到預期的結果 ```c x = 0100000000000000 // x = 2^15 (in decimal) ``` 欲在 64 位元的大小完成以上實作,嘗試改寫為以下: ```c uint64_t next_pow2(uint64_t x) { x |= x >> 1; x |= x >> 1; x |= x >> 1; x |= x >> 1; x |= x >> 1; x |= x >> 1; x |= x >> 1; x |= x >> 8; // AAAA = 8 x |= x >> 16; x |= x >> 32; // BBBB = 32 return x + 1; // CCCC = x + 1 } ``` 舉例來說, 64 位元的 `x = 00100...0` 丟進 `next_pow2` 執行結束後,會得到 `00111...1 + 1 = 01000...0` ,換成十進位為 $2^{62}$ ,即為預期的輸出 :::success 延伸問題 1. 解釋上述程式碼原理,並用 [`__builtin_clzl`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 改寫 > `int __builtin_clz (unsigned int x)` > Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined. > `int __builtin_clzl (unsigned long)` > Similar to `__builtin_clz` , except the argument type is unsigned long. 2. 在 Linux 核心原始程式碼找出類似的使用案例並解釋 3. 當上述 `clz` 內建函式已運用時,編譯器能否產生對應的 x86 指令? > 提示: 可執行 `cc -O2 -std=c99 -S next_pow2.c` 並觀察產生的 `next_pow2.s` 檔案,確認 `bsrq` 指令 (表示 “[Bit Scan Reverse](https://c9x.me/x86/html/file_module_x86_id_20.html)”) ::: 1. 解釋上述程式碼原理,並用 `__builtin_clzl` 改寫 ```c uint64_t next_pow2(uint64_t x) { uint64_t num = __builtin_clzl(x); x >>= (64 - num); x = 1; x <<= (64 - num); return x; } int main() { uint64_t input = 128; printf("%lu\n", next_pow2(input - 1)); return 0; } ``` 我的作法是使用 `__builtin_clzl` 取得 `x` 的領頭 0-bits 數量,間接得知最接近最高位元的 1-bit 所在的是第幾個位元,於是我使用右移位移運算子將左邊出現的第一個 1-bit 移至最低位元的右邊,也就是說我將所有 1-bits 清除,如下例 ```c uint64_t x = 0010...01; // num = 1 x >>= (64 - num); // x = 0000...00 ``` 接著將 `x` 代 1 ,再將它左移至原本最左邊出現的 1-bit 的左邊一個位元 ```c x = 1; // x = 0000...01 x <<= (64 - num); // x = 0100...00 ``` 至此步驟已經實現了找出大於 `x` 的最小 2 的次冪,不過根據題意,當 `x` 剛好是 2 的次冪時,應回傳原本的數,當我正要開始思考判斷式該如何寫時,< [fewletter](https://hackmd.io/@fewletter/HJF4nKaCi#%E8%A7%A3%E9%87%8B%E4%B8%8A%E8%BF%B0%E7%A8%8B%E5%BC%8F%E7%A2%BC%E5%8E%9F%E7%90%86%EF%BC%8C%E4%B8%A6%E7%94%A8-__builtin_clzl-%E6%94%B9%E5%AF%AB) >提出了一個很棒的想法,我們看下面這行程式碼 ```c next_pow2(input - 1) ``` `input` 是要丟入 `next_pow2()` 執行的 64 位元數值,若將 `input - 1` 丟入 `next_pow2()` 中執行,就可以達成前述的效果 2. 在 Linux 核心原始程式碼找出類似的使用案例並解釋 我找到的類似應用是 Linux V4L2 ,它是一種用於攝影機的驅動程式,一些重要參數的設定都被包含在 V4L2 中,下方是部份的程式碼 ```c void example_bitwise_usage(struct v4l2_format *fmt) { // 檢查是否有 XRGB 格式的支援 if ((fmt->fmt.pix.pixelformat & 0xFFFF) == ('X' | ('R' << 8) | ('G' << 16) | ('B' << 24))) { fmt->fmt.pix.pixelformat = V4L2_PIX_FMT_XRGB32; } } ``` 首先觀察 `if` 判斷式的 `==` 左邊, `fmt->fmt.pix.pixelformat` 代表像素格式的設定,因此這裡是在提取目前的格式設定。而 `==` 右邊則是 XRGB 的格式,`'x', 'R', 'G', 'B'` 個別代表透明度、紅、綠、藍色的像素格式識別碼,它們各佔有 8 bits ,總共有 32 bits 。若 `if` 判斷式滿足即表示目前的格式設定為 XRGB 格式,代表系統支援 XRGB ,則可將像素格式設定為 `V4L2_PIX_FMT_XRGB32` 3. 當上述 `clz` 內建函式已運用時,編譯器能否產生對應的 x86 指令? > 提示: 可執行 `cc -O2 -std=c99 -S next_pow2.c` 並觀察產生的 `next_pow2.s` 檔案,確認 `bsrq` 指令 執行後產生的 `.s` 檔部份程式碼 ```c next_pow2: .LFB13: .cfi_startproc endbr64 bsrq %rdi, %rdi movl $64, %ecx movl $1, %eax xorq $63, %rdi subl %edi, %ecx salq %cl, %rax ret .cfi_endproc ``` 結果顯示有產生對應的 `bsrq` 指令 ### 測驗 `2` ```c int concatenatedBinary(int n) { const int M = 1e9 + 7; int len = 0; long ans = 0; for (int i = 1; i <= n; i++) { if (!(i & (i - 1))) // DDDD = i & (i - 1) len++; ans = (i | (ans << len)) % M; // EEEE = ans << len } return ans; } ``` `if` 判別式在 `for` 迴圈中的作用是判斷 `i` 是否為 2 的次冪,判別方式如下 ```c i = 1000 // (8 in dec) // so (i - 1) = 111 if (!(i & (i - 1))) == if (!(1000 & 111)) == if (true) ``` 判斷 2 的次冪是為了要進行位元左移, `i` 的值不斷增加,當剛好變為 2 的次冪時,二進位表示法即會進位一次,此時位元左移的次數就要加 1 ```c i | (ans << len) ``` 由 `|` 將 `i` 與左移的 `ans` 接在一起 :::info 詢問 ChatGPT 上述程式碼中 `M` 的用途 ![](https://i.imgur.com/TpuqvG8.png) 得知 `M` 的用途是為了限制結果不要超出範圍以外,稱之為「對 M 取模」 ::: :::success 延伸問題 1. 嘗試使用 [`__builtin_{clz,ctz,ffs}`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 改寫,並改進 mod $10^9+7$ 的運算 ::: 1. 使用 `__builtin_clz()` 與 `__builtin_ctz()` 重新實作 以下為更動的程式碼 ```c if ((__builtin_clz(i) + __builtin_ctz(i) + 1) == 32) ``` 使用 `__builtin_clz()` 跟 `__builtin_ctz()` 這兩個函式,我就可以找出從頭端跟從尾端數到 1-bit 為止的 0-bits 的數量,假如 `i` 是 2 的次冪的話,前述兩個函式的總和值 +1 就會等於 `int` 資料型態的空間大小 4 bytes ( 32 bits ) 2. 改進 mod $10^9+7$ 的運算 題目中的 `concatenatedBinary()` 回傳之資料型態為 `int` ,但 `ans` 的資料型態為 `long int` ,經過 `M` 取模後可能會有 overflow 的問題,原因是 `M` 所取的質數為 $10^9 + 7$ ,連 `int` 最大值 2,147,483,647 的一半都還不到,若想完整地串連起來擁有較大值的 `int` 型態級數就會出現問題 為了改進 mod $10^9+7$ 的運算,以減少 overflow 的可能性,可以將 `M` 取值為比 $2^{k-1}$ 還要小的最大質數 ( k 為 `ans` 資料型態所佔用的 bits 數) ,故可取 `M` $= 2^{63} - 59$ ,不過函式的傳回值必須改為 `long` ```c const uint64_t M = (UINT64_C(1) << 63) - UINT64_C(59); ``` 使用修改後的程式碼重新測試 `n = 12` 時的結果為 $118,505,380,540$ 共 12 位數,近似於 $2^{36}$ ,比原本取模 `M` $= 10^9 + 7$ 時的結果還要大,且也符合真正的位元數 ```c // 1,2,...,12 end to end in binary 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 -> 1 1011 1001 0111 0111 1000 1001 1010 1011 1100 ( 37 bits ) ``` 雖然結果符合預期,但光是 `n = 12` 就需要佔用 37 bits 的空間,約為 `M` 所佔用空間 64 bits 的一半,因此能操作的 `n` 值上限不高,為了避免 overflow 的潛在不安因素,可以加入判斷式限制 `n` 的值不要超過上限即可 假如 `n = 25` 為安全範圍的上限,進行例外排除 ```c if (n > 25) return 0; ``` ### 測驗 `3` 參考 < [seasonwang](https://hackmd.io/@seasonwang/S1iI3p6Rj#%E6%B8%AC%E9%A9%97-3) > ,修正編譯有誤的片段程式,如下所示 ```diff size_t swar_count_utf8(const char *buf, size_t len) { const uint64_t *qword = (const uint64_t *) buf; - const uint64_t *end = qword + len >> 3; + const usin64_t *end = qword + (len >> 3); size_t count = 0; for (; qword != end; qword++) { const uint64_t t0 = *qword; const uint64_t t1 = ~t0; const uint64_t t2 = t1 & 0x04040404040404040llu; const uint64_t t3 = t2 + t2; const uint64_t t4 = t0 & t3; count += __builtin_popcountll(t4); } count = (1 << 3) * (len / 8) - count; // AAAA = 3 count += (len & 7) ? count_utf8((const char *) end, len & 7) : 0; // BBBB = 7 , CCCC = 7 , DDDD = 0 return count; } ``` 1. 上述程式碼的運作原理,建立的 `qword` 指向 `buf` 的地址,而 `end` 則指向 `qword[len >> 3]` 的地址, `len >> 3` 相當於 `len / 8` 2. 接著 `for` 迴圈的作用是計算 `qword` 到 `end` 之間**不為 UTF-8 的 bytes 數**,配合 `__builtin_popcountll` 計算個數 3. `(1 << 3) * (len / 8)` 會得到 8 的倍數,再扣除 `count` 後可得 UTF-8 的總數 4. `len & 7` 可視為 `len % 8` 的反義,若 `len` 為 8 的倍數,則 `len & 7` 為 `false` ,故 `count += 0` 5. 反之若 `len` 不為 8 的被數,則進入函式 `count_utf8` 以計算剩餘的 bytes 是否還有 UTF-8 ,最後回傳的 `count` 即為 `buf` 中之 UTF-8 總數 ### 測驗 `4` ```c bool is_pattern(uint16_t x) { if (!x) return 0; for (; x > 0; x <<= 1) { if (!(x & 0x8000)) return false; } return true; } ``` 以上的程式碼是在判斷 `x` 的 Most Significant Bit ( MSB ) 是否為 1 ,並且它後面的位元為連續的 1-bit ,例如 ```c 1000 1110 1111 1111 ``` 若滿足這種型態,則回傳 `true` ,反之則回傳 `false` 若要將程式碼進行改寫,達成等價的行為,但更精簡有效,可以寫成如下形式 ```c bool is_pattern(uint16_t x) { const uint16_t n = ~x + 1; // EEEE = ~x + 1 return (n ^ x) < x; // FFFF = x } ``` 原理是藉由 `~x + 1` 使 `n` 中最接近 Least Significant Bit ( LSB ) 的 1-bit 所在的位元與 `x` 相同,如此一來便可藉由 `n XOR x` 將最靠近 LSB 的 1-bit 變為 0-bit ,而其他位元則如同執行 `OR` 運算,如下所示 ```c x = 10010010 n = 01101110 (n ^ x) = 11111100 ``` 上述的運算中,可以看到一個性質,就是 MSB 之後是否為連續的 1-bits 將會影響運算結果的大小,舉例來說 ```c // case 1 x = 10010010 (n ^ x) = 11111100 (n ^ x) > x // case 2 x = 11111110 (n ^ x) = 11111100 (n ^ x) < x ``` 藉由此性質,可以判斷 `x` 是否符合特定樣式 :::success 延伸問題 1. 在 Linux 核心原始程式碼找出上述 bitmask 及產生器,探討應用範疇 > 參見 [Data Structures in the Linux Kernel](https://0xax.gitbooks.io/linux-insides/content/DataStructures/linux-datastructures-3.html) ::: 1. 應用範疇 我找到的應用範疇是型別 `cpumask_t` ,它在 Linux Kernel 中被廣泛運用在檢視及操作 CPU 的狀態,位於 [`include/linux/bitmap.h`](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/include/linux/bitmap.h) 當中 以下為其中一個 API :::info `cpumask_of()` 是 Linux kernel 中一個用來創建 CPU 集合的函數,其原型定義如下: ```c cpumask_t cpumask_of(int cpu); ``` 該函數接受一個整數 cpu,返回一個包含指定 CPU 的位元遮罩 `cpumask_t` ,例如, `cpumask_of(0)` 會返回一個只有第一個 CPU 為 1,其他位元都為 0 的 `cpumask_t` 值。 `cpumask_of()` 函數常被用於創建進程的 CPU 選擇集合。在 Linux kernel 中,每個進程都可以綁定到一個或多個 CPU 上運行。進程的 CPU 選擇集合通常表示為一個 `cpumask_t` 位元遮罩,用來指定哪些 CPU 可以用於運行進程。 例如,以下代碼展示了如何使用 `cpumask_of()` 函數創建一個包含第一個 CPU 和第二個 CPU 的選擇集合: ```c #include <linux/cpumask.h> cpumask_t my_cpuset; cpumask_clear(&my_cpuset); // 初始化選擇集合為空 cpumask_set_cpu(0, &my_cpuset); cpumask_set_cpu(1, &my_cpuset); ``` 在這個例子中, `cpumask_clear()` 函數用來初始化 `my_cpuset` 為空集合。接下來, `cpumask_set_cpu()` 函數用來將第一個 CPU 和第二個 CPU 分別加入選擇集合中。 透過使用 `cpumask_of()` 函數,可以方便地創建包含指定 CPU 的選擇集合,進而用於進程的 CPU 調度和管理。 > ChatGPT ::: 它的功能是取得指定 CPU 的遮罩,得以檢視及更動該 CPU 的狀態 假如一項進程需要 5 個 CPU 運行,則可以透過 `cpumask_set_cpu` 來選擇要綁定的 5 個 CPU 運行進程