# 2023q1 Homework2 (quiz2) contributed by < `gaberplaysgame` > ## 測驗 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; } ``` 以上程式碼取得比 64 位元的無號整數更大的 2 的冪次,舉例若 `x = 0x7F0E`, 則回傳的值會是 `0x8000` = $2^{15}$。 當 x 右移後產生的值與原本的 `x` 進行 OR 運算並重複 63 次後,可以把第一個 1 以後的 0 皆設為 1,如上面的例子,`x` 會變成 `0x7FFF`。再對 x 進行遞增的動作後由於進位,即可得到 `0x8000` 的數值。 ### 利用 `__builtin_clzl` 改寫 > Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined. > > Similar to `__builtin_ctz`, except the argument type is **unsigned long**. 這邊要注意的是 `__builtin_ctzl` 只支援 `unsigned long`,但 `uint_64_t` 為 `unsigned long long`,顯然這裡需要進行處理。利用兩個 32 位元無號整數 `head` 與 `tail` 存入 `x` 的兩部分,並對 `head` 進行比較。 - 若 `head` 非零則對進行 `__builtin_ctzl(head)` - 若 `head` 為零則進行 `__builtin_ctzl(tail) + 32` 這邊若 head 為零不能直接進行 `__builtin_ctzl` 操作,此函數對零的操作並未定義。若使用 gcc 編譯後執行會得出 31 ,顯然與預期結果 32 不符。 ```c uint64_t next_pow2_32(uint64_t x) { uint32_t head = (uint32_t)(x >> 32), tail = (uint32_t)x; int leading_zero = head ? __builtin_clzl(head) : 32 + __builtin_clzl(tail); return pow2((uint8_t)(64 - leading_zero)); } ``` 另外用 `__builtin_clz` 其實有變體支援 `unsign long long`,以下是由 `__builtin_ctzll` 寫成的版本: ```c uint64_t next_pow2_64(uint64_t x) { return pow2((uint8_t)(64 - __builtin_clzll(x))); } ``` ### `__builtin_clz` 在 Linux 核心的應用案例 閱讀 [<asm/compiler.h>](https://elixir.bootlin.com/linux/latest/source/arch/alpha/include/uapi/asm/compiler.h#L55),可以看到此內建函式在 Linux 核心內被定義為 `__kernel_ctlz(x)`,它被應用在 `fls` 、 `fls64` 、 `fls_long` 等函式上,實際案例在 [<linux/bitop.h>](https://elixir.bootlin.com/linux/latest/source/include/linux/bitops.h#L228) 與 [<linux/log2.h>](https://elixir.bootlin.com/linux/latest/source/include/linux/log2.h#L57) 可見。 ```c static inline unsigned fls_long(unsigned long l) { if (sizeof(l) == 4) return fls(l); return fls64(l); } ``` ### 當上述 clz 內建函式已運用時,編譯器能否產生對應的 x86 指令? 執行 `gcc -O2 -std=c99 -S .\question_1.c` 後產生的組合語言檔案,可以看到 bsrl 有被產生,可證明編譯器有產生對應的 x86 指令。 ``` _next_pow2_32: LFB20: .cfi_startproc pushl %edi .cfi_def_cfa_offset 8 .cfi_offset 7, -8 pushl %esi .cfi_def_cfa_offset 12 .cfi_offset 6, -12 pushl %ebx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 subl $16, %esp .cfi_def_cfa_offset 32 movl 36(%esp), %esi movl 32(%esp), %edi movl $LC0, (%esp) movl %edi, 4(%esp) movl %esi, 8(%esp) call _printf testl %esi, %esi jne L34 bsrl %edi, %ebx xorl $31, %ebx addl $32, %ebx ``` ## 測驗 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; } ``` 可以看到 `DDDD` 的數值應填入 `i & (i - 1)`, `EEEE` 為 `ans << len`。 整數 len 在每次迴圈都會用 `i & (i - 1)` 判斷是否應該增加,這數值代表每次迴圈 ans 會進行的移位數。 - 若 `i = 4`,則 `4 & 3 = 0`,這是因為 2 的冪次只有一個位元為 1,且 `i - 1` 的同個位元在 `i` 為 2 的冪次的條件下一定為 0 的原因。 - 相反,若 `i = 3`,則 `3 & 2 = 0`,由於 3 並非 2 的冪次,與 `i - 1` 進行 AND 運算後並不會得到 0 。 ### 利用 `__builtin_{clz,ctz,ffs}` 改寫,並改進 mod $10^9 + 7$ 的運算 在我的 linux 電腦上代入 `n = 12` 得到 361394469 並非預期結果 505379714 ,仔細調查發現我的 long 是 32 位元,最大值為 2147483647 ,而模數為 1000000007 ,約是最大值的一半。把式子 `(i | (ans << len)) % M` 以數學表示可得$ans\times2^{len} + i \pmod M$,若 `ans` 小於並足夠大的情況下,直接乘上 `2^len` ,在 `len > 2` 的情況下會導致整數溢出,進而產生錯誤值。因此把 `ans` 設為 long long。 - 以第 11 次迴圈舉例, `ans = 406586234`。 - $2^{len} = 2^4 = 16$,$406586234\times16 = 6,505,379,744\approx6.5\times10^{9}$ > $2^{32}$,在此運算步驟會導致溢出。 再利用上一大題的 `__builtin_clz` 取代 `len` 的計算,可得以下程式碼: ```diff - long ans = 0; + long long ans = 0; ... for (int i = 1; i <= n; i++) { - if (!(i & (i - 1))) - len++; - ans = (i | (ans << len)) % M ; + ans = (i | (ans << (32 - __builtin_clz(i)))) % M; } ``` ## 測驗 3 ### 原理 ```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); // 這邊要括號,C 語言內加法的優先度高於右移 size_t count = 0; for (; qword != end; qword+=1) { 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; } ``` - count_uf8: 將 `str[0~len-1]` 依序進行檢驗。 - swar_count_uf8: 將 `str` 內的每八項(如 `str[0]~str[7]`)塞入 qword 內一次進行檢驗,若有多餘項(如 `str[8]`)則利用原本的 `count_uf8` 檢驗。 在第 16 行會對於產出的 `count` 進行進行調整,在 13 行所產出的 `count` 所代表的是 continuation bytes (後續位元組),要將字串長度減去後續位元組數量後才會得出字元數。 另第 17 行則是對塞不滿 qword 內的位元組進行檢驗,這裡的 `len & 7` 相當於 `len % 8` 。 ### 在 Linux 核心原始程式碼找出 UTF-8 和 Unicode 相關字串處理的程式碼 翻閱 [<include/linux/unicode.h>](https://elixir.bootlin.com/linux/latest/source/include/linux/unicode.h) 可以看到以下函式被定義,並在 [<fs/unicode/utf8-core.c>](https://elixir.bootlin.com/linux/latest/source/fs/unicode/utf8-core.c#L12) 中實作,用來判斷是否為合法的 UTF-8 字元。 ```c int utf8_validate(const struct unicode_map *um, const struct qstr *str); ``` ```c int utf8_validate(const struct unicode_map *um, const struct qstr *str) { if (utf8nlen(um, UTF8_NFDI, str->name, str->len) < 0) return -1; return 0; } EXPORT_SYMBOL(utf8_validate); ``` 其他有應用到的檔案如 [<fs/unicode/utf8n.h>](https://elixir.bootlin.com/linux/latest/source/fs/unicode/utf8n.h) 也有相關函式的實作,這裡就不列舉。 ## 測驗 4 ### 原理 ```c bool is_pattern(uint16_t x) { const uint16_t n = ~x + 1; return (n ^ x) < x; } ``` 針對 16 位元無號整數 `x`,若 `x` 的位元排列符合 `11...10...0` 的圖樣,則回傳 `true` 。故將 `n` 設為 `x` 位元反轉後加上 `1`,若 `x` 符合上述樣式則新的 `n` 會是 `0...010...0` 的圖樣。當 `n XOR x` 後會產出比 `x` 少一個 `1` 的數值,因此會比 `x` 還小。 - 若 `x = 11111000`,則 `n = 00001000` , `x ^ n = 11110000 < x`。 - 若 `x = 11101000`,則 `n = 00011000` , `x ^ n = 11110000 > x`。 - 若 `x = 11001000`,則 `n = 00111000` , `x ^ n = 11110000 > x`。 從上面的兩個例子可以看到若 `x` 最右邊的 `1` 在從左數來**第 5 位元**時,不論 `x` 樣式, `x ^ n` 的值一定為 `11110000`,故也只有 `11111000` 可以比 `x ^ n` 大,因此有獨一性。這個情況不論最右邊的 `1` 在從左數來第幾位元都會成立。 ### Linux 核心的 bitmask 及產生器 根據文件 [Data Structures in the Linux Kernel](https://0xax.gitbooks.io/linux-insides/content/DataStructures/linux-datastructures-3.html) 可以看到應用在 [<linux/bitop.h>](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/asm/bitops.h)。其中以下函式有用到遮罩,會根據 nr 在編譯時間是否為常數來分別進行不同的測試位元函數。 ```c #define test_bit(nr, addr) \ (__builtin_constant_p((nr)) \ ? constant_test_bit((nr), (addr)) \ : variable_test_bit((nr), (addr))) static __always_inline bool constant_test_bit(long nr, const volatile unsigned long *addr) { return ((1UL << (nr & (BITS_PER_LONG-1))) & (addr[nr >> _BITOPS_LONG_SHIFT])) != 0; } static inline int variable_test_bit(long nr, volatile const unsigned long *addr) { int oldbit; asm volatile("bt %2,%1\n\t" "sbb %0,%0" : "=r" (oldbit) : "m" (*(unsigned long *)addr), "Ir" (nr)); return oldbit; } ```