--- tags: linux2023 --- # 2023q1 Homework2 (quiz2) contributed by < `Ahsen-lab` > ## 測驗 `1` ```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; x |= x >> 16; x |= x >> 32; return x + 1; } ``` ### 程式碼原理 對輸入的 64-bits 無號整數 `x` ,將其最高位元的 `1` 到最低位元中的所有位元都設定為 `1`,最後回傳 `x + 1` ,即可得到 ==**大於**== 且最接近 `x` 的 2 的冪的值。 舉例來說 $$ x = 001\overbrace{00...000}^{61} $$ 最高位元到最低位元中的第一個 `1` 是位在第 62 位元,首先重複 7 次 `x |= x >> 1` 的操作,將 `x` 中最高位元的 `1` 右側的 7 個位元都設定為 `1` $$ x = 0011111111\overbrace{000...000}^{54} $$ 接著執行 `x |= x >> 8` 的操作,將第 55 位元往右的 8 個位元都設定為 `1` $$ x = 00\overbrace{1111111111111111}^{16}\overbrace{000...000}^{46} $$ 接著執行 `x |= x >> 16` 的操作,將第 47 位元往右的 16 個位元都設定為 `1` $$ x = 00\overbrace{111...111}^{32}\overbrace{000...000}^{30} $$ 接著執行 `x |= x >> 32` 的操作,將第 31 位元往右的 32 個位元都設定為 `1` $$ x = 00\overbrace{111...111}^{62} $$ 最後回傳 `x + 1` $$ x + 1 = 01\overbrace{000...000}^{62} $$ 如此便可得到大於且最接近 `x` 的 2 的冪的值。 當輸入的 `x` 是 2 的冪時 ( 例如 `2` 、 `8` 、 `16` 等等 ) ,上述程式會回傳錯誤的答案,因為題目要求的是找出最接近且 ==**大於等於**== 2 的冪的值,而上述程式只會回傳最接近且 ==**大於**== 2 的冪的值。 換句話說,如果輸入的 `x` 是 2 的冪時,只需回傳 `x` 本身,不需要再尋找比它更大的 2 的冪。因此,上述程式在 `x` 是 2 的冪時會回傳錯誤的答案。 ### 使用 `__builtin_clzl` 改寫 ```c uint64_t next_pow2(uint64_t x) { if (!x) return 0; int lead_0 = __builtin_clzl(x); uint64_t test = (uint64_t) 1 << (63 - lead_0); if (!lead_0) return test; if (x > test) return test << 1; else return x; } ``` ## 測驗 `2` ```c int concatenatedBinary(int n) { const int M = 1e9 + 7; int len = 0; /* the bit length to be shifted */ /* use long here as it potentially could overflow for int */ long ans = 0; for (int i = 1; i <= n; i++) { /* removing the rightmost set bit * e.g. 100100 -> 100000 * 000001 -> 000000 * 000000 -> 000000 * after removal, if it is 0, then it means it is power of 2 * as all power of 2 only contains 1 set bit * if it is power of 2, we increase the bit length */ if (!(i & i - 1)) len++; ans = (i | (ans << len)) % M; } return ans; } ``` ### 程式碼原理 給定一個整數 $n$ ,將 1 到 $n$ 的二進位表示法依序串接在一起得到一個二進位字串,回傳其所代表的十進位數字 mod $10^9+7$ 之值。 使用一個迴圈從 1 遍歷到 $n$,如果當前遍歷到的數字是 2 的冪,增加 `len` 的長度, `len` 為當前數字其二進位表示的長度,然後將其與上一個結果串接起來,最終得到新的結果。 在函式中使用 `!(i & i - 1)` 來判斷 `i` 是否為 2 的冪,如果一個數字 `i` 是 2 的冪, `i - 1` 會剛好等於 `~i` ,而 `i & ~i` 會等於 `0` ,因此可以透過 `!(i & i - 1)` 來判斷 `i` 是否為 2 的冪。 在遍歷 1 到 $n$ 的過程中,使用 `i | (ans << len)` , `ans` 代表上一次計算所得的結果,使用了 `long` 型別的變數來存儲它,以避免整數溢出問題,`len` 為當前數字其二進位表示的長度,也是 `ans` 需向左位移的距離, `ans` 會向左位移 `len` 個 bits ,以便將 `i` 串接在後面。 最終,函式返回一個整數,即串接後的二進位表示對 $10^9+7$ 取模的結果。 ### 使用 `__builtin_clz` 改寫 ```c int concatenatedBinary(int n) { const int M = 1e9 + 7; int len = 0; /* the bit length to be shifted */ /* use long here as it potentially could overflow for int */ long ans = 0; int int_bits = 0; for (int i = 1; i <= n; i++) { /* removing the rightmost set bit * e.g. 100100 -> 100000 * 000001 -> 000000 * 000000 -> 000000 * after removal, if it is 0, then it means it is power of 2 * as all power of 2 only contains 1 set bit * if it is power of 2, we increase the bit length */ int_bits = sizeof(int) * 8; if (!(i & ~(1 << int_bits - __builtin_clz(i) - 1))) len++; ans = (i | (ans << len)) % M; } return ans; } ``` `__builtin_clz` 函式會返回一個整數的二進制表示中,左起第一個 1 之前的 0 的個數,可以利用此功能來改寫程式中判斷一個整數是不是 2 的冪的部分。 首先我給定一個變數 `int_bits` 表示 `int` 型別的位元數,計算方式為 `sizeof(int) * 8` , `sizeof(int)` 回回傳 `int` 型別的位元組 (byte) 數,每個位元組由 8 個位元組成,因此 `sizeof(int) * 8` 可得到 `int` 型別的位元數。 接著用 [你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise#Clear-a-bit) 所提到的 clear a bit 的操作來將整數 `i` 二進制表示中最左側的 `1` 設為 `0` ,透過 `~(1 << int_bits - __builtin_clz(i) - 1)` 可以找到要將其設為 `0` 的那個位元,然後把它與 `i` 做 and 運算,如此便可將整數 `i` 二進制表示中最左側的 `1` 變為 `0`。 若 `i` 為 2 的冪,其二進制表示中應該只包含一個 `1`,將其設為 `0` 會使得整數 `i` 變成 `0` ,因此若是在經過上述的操作後 `i` 變成 `0` ,就表示 `i` 是 2 的冪。 ### 使用 `__builtin_ctz` 改寫 ```c int concatenatedBinary(int n) { const int M = 1e9 + 7; int len = 0; /* the bit length to be shifted */ /* use long here as it potentially could overflow for int */ long ans = 0; for (int i = 1; i <= n; i++) { /* removing the rightmost set bit * e.g. 100100 -> 100000 * 000001 -> 000000 * 000000 -> 000000 * after removal, if it is 0, then it means it is power of 2 * as all power of 2 only contains 1 set bit * if it is power of 2, we increase the bit length */ if (!(i & ~(1 << __builtin_ctz(i)))) len++; ans = (i | (ans << len)) % M; } return ans; } ``` `__builtin_ctz` 函式會返回一個整數的二進制表示中,右起第一個 1 右側的 0 的個數,可以利用此功能來改寫程式中判斷一個整數是不是 2 的冪的部分。 使用 `~(1 << __builtin_ctz(i))` 可以找到要將其設為 `0` 的那個位元,然後把它與 `i` 做 and 運算,如此便可將整數 `i` 二進制表示中最右側的 `1` 變為 `0`。 若 `i` 為 2 的冪,其二進制表示中應該只包含一個 `1`,將其設為 `0` 會使得整數 `i` 變成 `0` ,因此若是在經過上述的操作後 `i` 變成 `0` ,就表示 `i` 是 2 的冪。 ### 使用 `__builtin_ffs` 改寫 ```c int concatenatedBinary(int n) { const int M = 1e9 + 7; int len = 0; /* the bit length to be shifted */ /* use long here as it potentially could overflow for int */ long ans = 0; for (int i = 1; i <= n; i++) { /* removing the rightmost set bit * e.g. 100100 -> 100000 * 000001 -> 000000 * 000000 -> 000000 * after removal, if it is 0, then it means it is power of 2 * as all power of 2 only contains 1 set bit * if it is power of 2, we increase the bit length */ if (!(i & ~(1 << __builtin_ffs(i) - 1))) len++; ans = (i | (ans << len)) % M; } return ans; } ``` `__builtin_ffs` 函式會返回一個整數的二進制表示中,右起第一個 1 的位置,假設 `i = 00001000` ,則 `__builtin_ffs(i)` 會返回 `4` ,可以利用此功能來改寫程式中判斷一個整數是不是 2 的冪的部分。 使用 `~(1 << __builtin_ffs(i) - 1)` 可以找到要將其設為 `0` 的那個位元,然後把它與 `i` 做 and 運算,如此便可將整數 `i` 二進制表示中最右側的 `1` 變為 `0`。 若 `i` 為 2 的冪,其二進制表示中應該只包含一個 `1`,將其設為 `0` 會使得整數 `i` 變成 `0` ,因此若是在經過上述的操作後 `i` 變成 `0` ,就表示 `i` 是 2 的冪。 ## 測驗 `3` ### 函式 `swar_count_utf8` 運作原理 ```c 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); 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; count += (len & 7) ? count_utf8((const char *) end, len & 7) : 0; return count; } ``` `swar_count_utf8` 函式使用 SWAR 的技巧來計算一個給定的 UTF-8 字串中的 Unicode 字元數量。 ```c const uint64_t *qword = (const uint64_t *) buf; const uint64_t *end = qword + (len >> 3); ``` 首先,把 UTF-8 字串 `buf` 轉換成一個指向 `uint64_t` 整數類型的指標 `qword` ,這樣就可以按照 `uint64_t` 的位數(64 位)進行處理,便於後續計算。接著計算 `qword` 的終止條件 `end` 。 `end` 的計算方式為 `qword + (len >> 3)` , `len >> 3` 相當於 `len / 8` ,( 因為 `uint64_t` 的大小是 8 個位元組 ),然後加上 `qword` 的初始值,得到 `end` 的位置,也就是將 `buf` 所占的總位元組每 8 個為一組分組後,剩餘的位元組開頭的位置 ( UTF-8 字串 `buf` 所占的總位元組數 `len` 不一定是 8 的倍數,因此可能會有剩餘 )。 ```c 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); } ``` 然後,對於每個 64-bit 無號整數,使用以下步驟操作來計算其中後續位元組 ( continuation byte(s) ) 的數量: > 參考 [2023q1 第 2 週測驗題 - 測驗 3](https://hackmd.io/@sysprog/linux2023-quiz2#%E6%B8%AC%E9%A9%97-3) 以 `t0 = [11xx.xxxx|10xx.xxxx|01xx.xxxx|00xx.xxxx|11xx.xxxx|10xx.xxxx|01xx.xxxx|00xx.xxxx]` 舉例 1. 輸入的位元組 ```c t0 = [00xx.xxxx|01xx.xxxx|10xx.xxxx|11xx.xxxx|00xx.xxxx|01xx.xxxx|10xx.xxxx|11xx.xxxx] ^^^^^^^^^ ^^^^^^^^^ continuation byte continuation byte ``` 2. 反向運算 (not bit6) ```c t1 = ~t0 t1 = [11xx.xxxx|10xx.xxxx|01xx.xxxx|00xx.xxxx|11xx.xxxx|10xx.xxxx|01xx.xxxx|00xx.xxxx] ``` 3. 隔離反向的第 6 個位元 ```c t2 = t1 & 0x4040404040404040 t2 = [0100.0000|0000.0000|0100.0000|0000.0000|0100.0000|0000.0000|0100.0000|0000.0000] ``` 4. 對第 6 個位元,向左位移 1 個位元 (移至第 7 個位元的位置) ```c t3 = t2 + t2 t3 = [1000.0000|0000.0000|1000.0000|0000.0000|1000.0000|0000.0000|1000.0000|0000.0000] ``` 5. 進行 `not bit6 and bit7` ```c t4 = t0 & t3 t4 = [00xx.xxxx|01xx.xxxx|10xx.xxxx|11xx.xxxx|00xx.xxxx|01xx.xxxx|10xx.xxxx|11xx.xxxx] & [1000.0000|0000.0000|1000.0000|0000.0000|1000.0000|0000.0000|1000.0000|0000.0000] = [0000.0000|0000.0000|1000.0000|0000.0000|0000.0000|0000.0000|1000.0000|0000.0000] ^^^^^^^^^ ^^^^^^^^^ the non-zero byte the non-zero byte ``` 6. 以 `__builtin_popcountll` 計算,即 `__builtin_popcountll(t4)`,並將結過加到 `count` 裡。 7. 重複上述步驟,最後得到的 `count` 即為後續位元組 ( continuation byte(s) ) 的數量。 ```c count = (1 << 3) * (len / 8) - count; count += (len & 7) ? count_utf8((const char *) end, len & 7) : 0; return count; ``` `(1 << 3) * (len / 8)` 將 `len` 除以 8,然後乘以 8(即左移 3 位),可使得位元組數量變成 8 的倍數 ( 為了配合 `uint64_t` 的位數(8 bytes)進行處理 ),接著減去後續位元組 ( continuation byte(s) ) 的數量,就可得到 UTF-8 字元的數量。 因為 UTF-8 字串 `buf` 所占的總位元組數 `len` 不一定是 8 的倍數,所以要計算 `len % 8` 來判斷 `end` 是否有指向 `buf` 尾端剩餘的位元組,當除數為 $2^n$ 時,可以 `len & n - 1` 來表示對 $n$ 做模運算,因此 `len % 8` 可改寫為 `len & 7`,假設所得的餘數大於 `0` ,則使用 `count_utf8` 函式來處理剩餘的位元組,計算剩餘的位元組中所包含的 UTF-8 字元的數量,並將結果加到 `count` ,若餘數等於 `0` 則將 `0` 加到 `count`,最後回傳 `count` 。 ## 測驗 `4` ```c bool is_pattern(uint16_t x) { const uint16_t n = ~x + 1; return (n ^ x) < x; } ``` ### 程式碼原理 ``` pattern: 8000 (1000 0000 0000 0000) pattern: c000 (1100 0000 0000 0000) pattern: e000 (1110 0000 0000 0000) pattern: f000 (1111 0000 0000 0000) pattern: f800 (1111 1000 0000 0000) pattern: fc00 (1111 1100 0000 0000) pattern: fe00 (1111 1110 0000 0000) pattern: ff00 (1111 1111 0000 0000) pattern: ff80 (1111 1111 1000 0000) pattern: ffc0 (1111 1111 1100 0000) pattern: ffe0 (1111 1111 1110 0000) pattern: fff0 (1111 1111 1111 0000) pattern: fff8 (1111 1111 1111 1000) pattern: fffc (1111 1111 1111 1100) pattern: fffe (1111 1111 1111 1110) pattern: ffff (1111 1111 1111 1111) ``` 從上面的 pattern 可看出,函式 `is_pattern` 會篩選出 16 位無號整數 `x` 的二進制表示中,有連續的 `1` 集中在左側而連續的 `0` 集中在右側的數。 精簡版的程式碼使用了二補數的概念,對輸入的無號整數 `x` 取反加 1 得到 `n`,然後將 `n` 與 `x` 做 XOR 運算,如果 `x` 符合上述的 pattern,`n` 會是 2 的冪,其值為 `x` 的二進制表示中,最右側那個 `1` 的位元所代表的數,所以 `n ^ x` 計算的結果相當於消除 `x` 的二進制表示中最右側那個 `1` ,因此,如果 `(n ^ x) < x` ,則表示 `x` 符合上述 pattern,這就是 `is_pattern` 函式要檢查的模式,因此返回 true,否則返回 false。