# 2023q1 Homework2 (quiz2) contributed by < [seasonwang0905](https://github.com/seasonwang0905) > > [測驗題目](https://hackmd.io/@sysprog/linux2023-quiz2) ## 測驗 `1` ### 解釋程式碼運作原理 根據題目,將 `next_pow2` 改寫為以下程式碼 ```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 >> AAAA; x |= x >> 16; x |= x >> BBBB; return CCCC; } ``` 此題答案為 * `AAAA` = `8` * `BBBB` = `32` * `CCCC` = `x + 1` 假設輸入值 `x` 為 51 ,則 `next_pow2` 的回傳值為無號數 64 ,這裡用 16 bits 闡述其原理: 1. `x |= x >> 1` 表 **`x` 右移 1 個 bit ,再和 `x` 做 `|` 位元運算並賦值給 `x`** ```c 0000 0000 0011 0011 | 0000 0000 0001 1001 = 0000 0000 0011 1011 // x = 0000 0000 0011 1011 ``` 2. 做七次後可得 `x = 0000 0000 0011 1111` ,執行到 `x |= x >> 8` 時,因為 `x` 右移 8 bits 會得到 `0000 0000 0000 0000` ,故其結果仍為 `x = 0000 0000 0011 1111` ```c 0000 0000 0011 1111 | 0000 0000 0000 0000 = 0000 0000 0011 1111 // x = 0000 0000 0011 1111 ``` 3. 從第 2 步可得知,**當位移超過其最高位 bit 時,經 `|=` 運算後都會得到原本的值** ```c x |= x >> 16; // x = 0000 0000 0011 1111 x |= x >> 32; // x = 0000 0000 0011 1111 ``` 4. 最後計算 `x + 1` 並以 `uint64_t` 回傳,結果為 `next_pow2(51) = 64` ```c 0000 0000 0011 1111 + 0000 0000 0000 0001 = 0000 0000 0100 0000 // (uint64_t) 0b0000000001000000 = 64 ``` ### 使用 __builtin_clzl 改寫程式碼 * [__builtin_clzl](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 上述 `next_pow2` 函式可以使用 `__builtin_clzl` 簡化,關於此函式的描述如下 > `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. 根據描述,可以設計以下程式碼 :::spoiler 實際程式碼 ```c uint64_t next_pow2(uint64_t x) { if (!x) return 1; int n = __builtin_clzl(x); return x >> (63 - n) << (64 - n); } ``` ::: * 當 `x` 為 0 時,回傳 `1` * `int n` 用來紀錄 `__builtin_clzl` 回傳的值 * 將 `x` 右移 `(63 - n)` 個位元,保留 leading 1,再左移 `(64 - n)` 個位元,即進位 :::warning 後來發現題目要求回傳**大於或等於** 2 的冪的值,故將程式碼更改為以下 ::: :::spoiler 更新程式碼 ```diff uint64_t next_pow2(uint64_t x) { if (!x) return 1; int n = __builtin_clzl(x); + int k = __builtin_ctzl(x); + if (n + k == 63) + return x; return x >> (63 - n) << (64 - n); } ``` ::: [__builtin_ctzl](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 可以回傳 leading 1 後面的 0 位元之數量, 當 `if` 判斷 `x` 是 2 次冪的值時,則回傳 `x` 本身 ### 在 Linux 核心原始程式碼找出類似的使用案例並解釋 * 使用 [CodeBrowser](https://codebrowser.dev/linux/linux/lib/bitmap.c.html#19src) 可幫助理解 linux kernel 的內容 [bitmap](https://zh.wikipedia.org/zh-tw/BMP) 是一種由一系列 bits 所組成的陣列,在 linux kernel 巨量的程式碼之中,時常可以看到 bitmap 的身影,其常被應用在 [VMM (Virtual Memory Manager)](https://en.wikipedia.org/wiki/Virtual_Machine_Manager) 和追蹤硬體資源 在 [linux/blob/master/lib/bitmap.c](https://github.com/torvalds/linux/blob/master/lib/bitmap.c) 中可以找到 `__bitmap_shift_right` 的程式碼 ```c /** * __bitmap_shift_right - logical right shift of the bits in a bitmap * @dst : destination bitmap * @src : source bitmap * @shift : shift by this many bits * @nbits : bitmap size, in bits * * Shifting right (dividing) means moving bits in the MS -> LS bit * direction. Zeros are fed into the vacated MS positions and the * LS bits shifted off the bottom are lost. */ void __bitmap_shift_right(unsigned long *dst, const unsigned long *src, unsigned shift, unsigned nbits) { unsigned k, lim = BITS_TO_LONGS(nbits); unsigned off = shift/BITS_PER_LONG, rem = shift % BITS_PER_LONG; unsigned long mask = BITMAP_LAST_WORD_MASK(nbits); for (k = 0; off + k < lim; ++k) { unsigned long upper, lower; /* * If shift is not word aligned, take lower rem bits of * word above and make them the top rem bits of result. */ if (!rem || off + k + 1 >= lim) upper = 0; else { upper = src[off + k + 1]; if (off + k + 1 == lim - 1) upper &= mask; upper <<= (BITS_PER_LONG - rem); } lower = src[off + k]; if (off + k == lim - 1) lower &= mask; lower >>= rem; dst[k] = lower | upper; } if (off) memset(&dst[lim - off], 0, off*sizeof(unsigned long)); } ``` 根據程式碼, 推測 `__bitmap_shift_right` 的功能為: * 以 `src` 表示欲進行右位移的 bitmap , `shift` 為每次右位移的數量 * 當 `off + k` 仍小於總位元數量 `lim` 時,進入 `for` 迴圈。使用 `upper` 以及 `lower` 變數分別儲存位移後的上半段和下半段位元 * 使用 `dst[k]` 紀錄位移後的 bitmap 遮罩 `mask` 的目的為確認是否到 bitmap 的尾端了, `memset` 的功用則是當 `shift` 的大小超過 bitmap 時,在 `dst` 的後端補上 0 位元 ### 編譯器是否能產生對應的 x86 指令 輸入命令 `cc -O2 -std=c99 -S test.c` ,產生的 `test.s` 包含下列指令 ``` next_pow2: .LFB14: .cfi_startproc endbr64 movl $1, %eax testq %rdi, %rdi je .L1 bsrq %rdi, %rax movl $63, %ecx xorq $63, %rax subl %eax, %ecx shrq %cl, %rdi movl $64, %ecx subl %eax, %ecx movq %rdi, %rax salq %cl, %rax ``` 檢查有 `bsrq` 指令,所以編譯器可以產生對應的 x86 指令 --- ## 測驗 `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 (!(DDDD)) len++; ans = (i | (EEEE)) % M; } return ans; } ``` 此題答案為 * `DDDD` = `i & (i - 1)` * `EEEE` = `ans << len` `if` 判斷式的作用為: 1. 在 `i = 1` 時,讓 `len` 增加為 1 2. 檢查 `i` 值是否為 2 次冪,如果是,則 `len` 增加 1 以二進位而言,每當數值恰為 2 次冪時,因為多一個位元,需要位移及串接的長度便會增加, `% M` 是為了防止整數產生 overflow 的情形 :::info [$mod\ \ {10^9+7}$](https://www.geeksforgeeks.org/modulo-1097-1000000007/) 是普遍用來防止整數產生 overflow 的方式,其可以保證可能導致 overflow 的數值輸入到程式中時,它們仍然會在有效的數值範圍中被處理,像這樣的運算方式稱為[「取模運算」(modulo operation)](https://zh.wikipedia.org/zh-tw/%E6%A8%A1%E9%99%A4) , C 語言中的模數須符合兩個條件: 1. 模數必須在整數數值範圍之內,而且足夠大 2. 模數必須是質數,取模運算後的結果更加離散,即結果不易重複,這可以提高演算法的正確性和可靠性 1000000007 符合上述兩個條件,故常被用來當作模數 ::: 以上述程式碼舉個例子,假設輸入 `n` 等於 3,已知數值不會 overflow ,故可忽略 `% M` 1. `i = 1` ,進入迴圈後遇 `if` 判斷式,判斷式為 `true` ,執行 `len++` ```c 0000 0001 & 0000 0000 = 0000 0000 // i = 0000 0001, i - 1 = 0000 0000 // if (!0) -> execute statement ``` 2. `ans` 左移 1 格再和 `i` 做 `|` 運算,最後賦值給 `ans` ```c 0000 0001 | 0000 0000 = 0000 0001 // ans = 0000 0001 ``` 3. `i = 2` ,`if` 判斷式為 `true` ,執行 `len++` ```c 0000 0010 & 0000 0001 = 0000 0000 // i = 0000 0010, i - 1 = 0000 0001 // if (!0) -> execute statement ``` 4. `ans` 左移 2 格再和 `i` 做 `|` 運算,最後賦值給 `ans` ```c 0000 0010 | 0000 0100 = 0000 0110 // ans = 0000 0110 ``` 5. `i = 3` ,`if` 判斷式為 `false` ```c 0000 0011 & 0000 0010 = 0000 0010 // i = 0000 0011, i - 1 = 0000 0010 // if (!2) -> skip ``` 6. 同 4. ```c 0000 0011 | 0001 1000 = 0001 1011 // ans = 0001 1011 ``` 將 `ans` 換算成十進位可得 `ans = 27` ### 嘗試使用 `__builtin_{clz,ctz,ffs}` 改寫 * [`__builtin_ffs`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) > Returns one plus the index of the least significant 1-bit of x, or if x is zero, returns zero. 根據描述,`__builtin_ffs` 可以回傳第一個設置的位元之位置,第一個設置的位元指的是從右往第一個為 1 的位元,例如 * 8 在二進位為 `0b1000` ,`__builtin_ffs(8)` 會回傳整數 4 * 3 在二進位為 `0b0011` ,`__builtin_ffs(3)` 則會回傳整數 1 將 `concatenatedBinary` 用 `__builtin_clz`, `__builtin_ctz`, `__builtin_ffs` 改寫,假設先忽略模數 `M` ,即不考慮 overflow ,可以設計以下程式碼 :::spoiler 實際程式碼 ```c int concatenatedBinary(int n) { int len = 0; long ans = 0; for (int i = 1; i <= n; i++) { if (__builtin_clz(i) + __builtin_ctz(i) == 31) len = __builtin_ffs(i); ans = (i | (ans << len)); } return ans; } ``` ::: * `if (__builtin_clz(i) + __builtin_ctz(i) == 31)` 判斷 `i` 是否為 2 次冪,若是,則 `i` 只會有一個 1 位元,再用 `__builtin_ffs(i)` 回傳第一個設置的位元之位置,即 `len` * `ans` 的算法與之前相同 測試結果正確無誤,接著加入模數 `M` 並改進取模運算 ### 改進取模運算 考慮取模運算的作用,我們需要在可能發生 overflow 地方加上 `% M` :::spoiler 實際程式碼 ```c int concatenatedBinary(int n) { const int M = 1e9 + 7; int len = 0; long ans = 0; for (int i = 1; i <= n; i++) { if (__builtin_clz(i) + __builtin_ctz(i) == 31) len = __builtin_ffs(i); ans = (i | (ans << len) % M); } return ans; } ``` ::: * `n` 和 `i` 在函式內不會發生 overflow ,故不用對其做取模運算 * 若 `ans` 本身夠大,左移時便可能產生 overflow ,故在 `ans << len` 後方加上 `% M` * `(i | (ans << len) % M)` 不會發生 overflow ,後方不需要加上 `% M` 更改 `% M` 的位置後,發現其結果與 [測驗 `2`](https://hackmd.io/@sysprog/linux2023-quiz2#%E6%B8%AC%E9%A9%97-2) 測試資料 3 的結果略有不同 測試資料 3: ```c // n = 12 ans = 505379714 ``` 更改後: ```c // n = 12 ans = 505379534 ``` 從結果推測,在測試資料 3 中, `ans` 左移 `len` 時有發生過 overflow 而導致輸出不同 --- ## 測驗 `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 << AAAA) * (len / 8) - count; count += (len & BBBB) ? count_utf8((const char *) end, len & CCCC) : DDDD; return count; } ``` 作上述改動後即可運行 此題可寫為 * `AAAA` = `3` * `BBBB` = `7` * `CCCC` = `7` * `DDDD` = `0` 將每一行程式碼拆解來看: 1. `len` 為傳入字串 `buf` 的長度,單位為 bytes ,傳進的字串以 64 位元來處理,建立 `qword` 指向 `buf` 的位址, `end` 則指向 `qword[len >> 3]` 之位址。 `len >> 3` 相當於 `len / 8` 2. `for` 迴圈內為計算 `qword` 到 `end` 之間,包含 `end` 指向的 `uint64_t` 中,**不為 UTF-8 的 bytes 數**,演算法配合 [`__builtin_popcountll`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 來計算個數並以 `count` 紀錄 3. `(1 << 3) * (len / 8)` 會得到 8 的整數倍,扣掉 `count` 後即為目前算得的 UTF-8 的個數 4. `len & 7` 在判斷式中可視為 `len % 8` 的反義,若 `len` 為 8 的倍數,則 `len & 7` 為 `false` ,執行 `count += 0`,最後回傳的 `count` 即為 `buf` 中 UTF-8 之總數 5. 若 `len` 不為 8 的倍數,則 `len & 7` 為 `true` ,進入函式 `count_utf8` 以計算剩餘的 bytes 是否含有 UTF-8 ,並加上此函式所得的 `count` ,最後回傳的 `count` 即為 `buf` 中 UTF-8 之總數。 ### 比較 SWAR 和原本的實作效能落差 ### --- ## 測驗 `4` ### 解釋程式碼運作原理 根據題目,下列程式碼是用來判斷特定形式的 `uint16_t` ```c bool is_pattern(uint16_t x) { if (!x) return 0; for (; x > 0; x <<= 1) { if (!(x & 0x8000)) return false; } return true; } ``` 經過輸出,特定的形式為以下幾者 ``` pattern: 8000 (32768) pattern: c000 (49152) pattern: e000 (57344) pattern: f000 (61440) pattern: f800 (63488) pattern: fc00 (64512) pattern: fe00 (65024) pattern: ff00 (65280) pattern: ff80 (65408) pattern: ffc0 (65472) pattern: ffe0 (65504) pattern: fff0 (65520) pattern: fff8 (65528) pattern: fffc (65532) pattern: fffe (65534) pattern: ffff (65535) ``` 觀察輸出的形式可知,若換算成二進位,從最高位到最低位皆為緊密排列的 1 位元,如 `0xc000` = `0b1100 0000 0000 0000` ,藉由這個觀察結果,我們可以依據上述觀察結果,將程式碼改寫為 ```c bool is_pattern(uint16_t x) { const uint16_t n = EEEE; return (n ^ x) < FFFF; } ``` 此題可寫入 * `EEEE` = `~x + 1` * `FFFF` = `x` 假設 `x` 為輸入的特定形式,則 1. `n` 取 `x` 之二補數再加 1 ,**此時 `n` 的最高位必定是 `x` 的第一位設置的位元** ```c // if x = 0xc000 1100 0000 0000 0000 // x 0100 0000 0000 0000 // ~x + 1 0100 0000 0000 0000 // n ``` 2. `n` 與 `x` 做 `^` (XOR) 運算,**其結果必小於 `x`** ```c 1000 0000 0000 0000 // n ^ x // (n ^ x) < x returns true ``` ### Linux 原始核心碼 : `bitmap_fill` 原本想寫 [Data Structures in the Linux Kernel](https://0xax.gitbooks.io/linux-insides/content/DataStructures/linux-datastructures-3.html) 裡提到的 `bitmap_fill` ,但後來在 Linux Kernel 搜尋時,發現在這段原始碼在 [include/linux/bitmap.h](https://github.com/torvalds/linux/blob/master/include/linux/bitmap.h) 中改版了,這裡節錄改版的資訊 > **include/linux/bitmap.h: make bitmap_fill() and bitmap_zero() consistent** > Behaviour of bitmap_fill() differs from bitmap_zero() in a way how bits behind bitmap are handed. bitmap_zero() clears entire bitmap by unsigned long boundary, while bitmap_fill() mimics bitmap_set(). > Here we change bitmap_fill() behaviour to be consistent with bitmap_zero() and add a note to documentation. > The change might reveal some bugs in the code where unused bits are handled differently and in such cases bitmap_set() has to be used. 上述提到這次的改版主要是讓 `bitmap_fill` 和 `bitmap_zero` 的行為保持一致性,否則在使用上可能會有 bug,以下為改版前後的程式碼 ```diff= static inline void bitmap_fill(unsigned long *dst, unsigned int nbits) { - unsigned int nlongs = BITS_TO_LONGS(nbits); - if (!small_const_nbits(nbits)) { - unsigned int len = (nlongs - 1) * sizeof(unsigned long); - memset(dst, 0xff, len); + if (small_const_nbits(nbits)) + *dst = ~0UL; + else { + unsigned int len = BITS_TO_LONGS(nbits) * sizeof(unsigned long); + memset(dst, 0xff, len); } - dst[nlongs - 1] = BITMAP_LAST_WORD_MASK(nbits); } ``` 可以看到兩版本的 `if` 判斷式中的描述互為反義, 根據 [`small_const_nbits`](https://github.com/torvalds/linux/blob/master/include/asm-generic/bitsperlong.h) 之定義,若 $1\ <=nbits\ <=\ 8$ 時,則 `small_const_nbits` 回傳 `true` 假設系統為 64 位元,在舊版陳述句中,若 `nbits` 不在前述範圍內,則填 `nlongs - 1` * 8 的長度的 `0xff` 給 `dst` 陣列,並且在最後的 9 個位元做了 mask (即第 13 行),而新版則因為一致性問題,捨去了 `BITMAP_LAST_WORD_MASK` ,直接將 `dst` 填滿 `~0UL` 或是 `0xff` 附上各巨集的定義以及來源 :::spoiler 巨集 > [BITS_TO_LONGS](https://github.com/torvalds/linux/blob/master/include/linux/bitops.h) ```c #define BITS_TO_LONGS(nr) __KERNEL_DIV_ROUND_UP(nr, BITS_PER_TYPE(long)) ``` > [__KERNEL_DIV_ROUND_UP](https://github.com/torvalds/linux/blob/master/include/uapi/linux/const.h) ```c #define __KERNEL_DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d)) ``` > [small_const_nbits](https://github.com/torvalds/linux/blob/master/include/asm-generic/bitsperlong.h) ```c /* * small_const_nbits(n) is true precisely when it is known at compile-time * that BITMAP_SIZE(n) is 1, i.e. 1 <= n <= BITS_PER_LONG. This allows * various bit/bitmap APIs to provide a fast inline implementation. Bitmaps * of size 0 are very rare, and a compile-time-known-size 0 is most likely * a sign of error. They will be handled correctly by the bit/bitmap APIs, * but using the out-of-line functions, so that the inline implementations * can unconditionally dereference the pointer(s). */ #define small_const_nbits(nbits) \ (__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG && (nbits) > 0) ``` > [BITMAP_LAST_WORD_MASK](https://github.com/torvalds/linux/blob/master/include/linux/bitmap.h) ```c #define BITMAP_LAST_WORD_MASK(nbits) (~0UL >> (-(nbits) & (BITS_PER_LONG - 1))) ``` :::