--- title: 2023q1 Homework2 (quiz2) tags: Linux核心實作 --- # 2023q1 Homework2 (quiz2) contributed by < [`lorian0738`](https://github.com/lorian0738) > ## [你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise) ( 隨記 ) 一個字元 1 byte (位元組) ### [數值系統](https://hackmd.io/@sysprog/c-numerics) * 數值系統的設計導致不同程式語言都會出現一些計算上的誤差 例如:0.1-0.01=0.09000000000000001 (python 2.7) #### 運用 bit-wise operator * 大小寫轉換 * 將字元轉成小寫: 免除使用分支 ('a' | '` `') // 得到 'a' ('A' | '` `') // 得到 'a' * 將字元轉為大寫: 免除使用分支 ('a' & '`_`') // 得到 'A' ('A' & '`_`') // 得到 'A' * 大小寫互轉: 避免使用分支 ('a' ^ '` `') // 得到 'A' ('A' ^ '` `') // 得到 'a' ``` 'a' 的二進位表示 1100001 'A' 的二進位表示 1000001 ' ' 的二進位表示 100000 '_' 的二進位表示 1011111 ``` #### 交換兩個記憶體空間內的數值,可完全不用額外的記憶體來實作 > 在 boot manager 或 boot loader 資源有限 > 主記憶體可能不能用 ```c void xorSwap(int *x, int *y) { *x ^= *y; *y ^= *x; *x ^= *y; } ``` #### 位移運算子 * 未定義狀況 1. 左移超過變數長度 ```c int i=0xFFFFFFFF; i = i << 32; // 此結果未定義 ``` 輸出: ``` main.c:14:11: warning: left shift count >= width of type [-Wshift-count-overflow] 14 | i = i << 32; // 此結果未定義 | ^~ ffffffff ``` 2. 右移一個負數時,可能是邏輯位移(左側補 0 )或是算術位移(補上號數) #### signed 和 unsigned 之間的關係 * 無窮迴圈 ```c int n = 10; for i = n - 1 ; i - sizeof(char) >= 0; i--) printf("i: 0x%x\n",i); ``` `sizeof` 不是 function 是 operator (運算子) => 回傳型態:unsigned interger 整數型態和 unsigned interger 做運算時,會整個變成 unsigned interger ## quiz 2 ### 測驗 1 考慮 next_pow2 可針對給定無號 64 位元數值 x,找出最接近且大於等於 2 的冪的值,例如: * next_pow2(7) = 8 * next_pow2(13) = 16 * next_pow2(42) = 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; x |= x >> 16; x |= x >> 32; return x+1; } ``` 上述程式碼將 x 和自己右移的值做 bitwise or 運算,目的是希望將比 x 的 MSB 低位的數字都變成 1,因此總共右移了 63 個位元,如此再 +1 進位後就會是想找的 2 的冪的值 :::info 疑惑點: 題目描述要找「最接近且大於等於 2 的冪的值」指的應該是最接近 x 且大於等於 x 的 2 的冪的值,但若是 x 本身就是 2 的冪,例如 1000₂ ,做完運算後會得到 10000₂ ,但依照題目敘述應該求的是 1000₂ ::: #### 1. 以 [__builtin_clzl](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 改寫 int __builtin_clzl (unsigned long) 會回傳前導 0 的位數,例如 input 為 9 (1001₂) 時,回傳 64 - 4 = 60 則可以知道最高有效位在最低位往高位數第 4 位,可知想求的值為 1 左移四個位數,即為 10000₂ ```c uint64_t next_pow2(uint64_t x) { int len = 64 - __builtin_clzl(x); return 1 << len; } ``` 經同學提醒,才想到 return 那邊若直接寫 1 會預設是 int ,因此若輸入超過 32 位元的值會溢位,需要特別標示型別為 `uint64_t`。 ```c uint64_t next_pow2(uint64_t x) { int len = 64 - __builtin_clzl(x); return (uint64_t)1 << len; } ``` #### 2. 在 Linux 核心原始程式碼找出類似的使用案例並解釋 #### 3. 當上述 clz 內建函式已運用時,編譯器能否產生對應的 x86 指令? > 提示: 可執行 cc -O2 -std=c99 -S next_pow2.c 並觀察產生的 next_pow2.s 檔案,確認 bsrq 指令 (表示 “Bit Scan Reverse”) ``` .file "next_pow2.c" .text .p2align 4 .globl next_pow2 .type next_pow2, @function next_pow2: .LFB13: .cfi_startproc endbr64 bsrq %rdi, %rdi movl $64, %ecx movl $1, %eax xorq $63, %rdi subl %edi, %ecx sall %cl, %eax cltq ret .cfi_endproc ... main: .LFB14: .cfi_startproc endbr64 subq $24, %rsp .cfi_def_cfa_offset 32 leaq .LC0(%rip), %rdi movq %fs:40, %rax movq %rax, 8(%rsp) xorl %eax, %eax movq %rsp, %rsi call __isoc99_scanf@PLT bsrq (%rsp), %rax ... ``` ### 測驗 2 LeetCode 1680. Concatenation of Consecutive Binary Numbers 給定一整數 n ,回傳將 1 到 n 的二進位表示法依序串接在一起所得到的二進位字串,其所代表的十進位數字 $mod\ {10^9}+7$ 之值。 ```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; } ``` #### 1. 解釋上述程式碼運作原理 此程式碼將 1 到 n 依序串接進 ans,若 `i & (i-1)` 為 0,表示 i 為 2 的冪,則 i 比 i - 1 的位數多一位,ans 在左移準備串接 i 時,要比串接 i - 1 時多左移一位,因此將記錄左移位數的 len + 1。 再利用左移足夠位數後填補的 0 的部分,與要串接的數字做 bitwise or 達到二進位制相加的效果。 例如當 n 為 4 時: ``` i = 1 時,1&0 為 0,len + 1,len = 1,ans = 1 i = 2 時,1&0 為 0,len + 1,len = 2,ans = 110 i = 3 時,1&0 非 0,len + 1,len = 2,ans = 11011 i = 4 時,1&0 為 0,len + 1,len = 3,ans = 11011110 ``` #### 2. 嘗試使用 __builtin_{clz,ctz,ffs} 改寫,並改進 $mod\ {10^9}+7$ 的運算 ``` clz:回傳 leading 0-bits ctz:回傳 trailing 0-bits ffs:回傳最低有效位索引值 + 1 ``` ```c int concatenatedBinary(int n) { const int M = 1e9 + 7; int len = 0; long ans = 0; for (int i = 1; i <= n; i++) { len = 32 - __builtin_clz (i); ans = (i | (ans << len)) % M; } return ans; } ``` ### 測驗 3 SWAR 實作如下: ```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; } ``` 首先把字串轉為 uint64_t 的形式存在 *qword 接著 len >> 3 即為 len / 8,長度除以 8 (無條件捨去到整數位) 可知該字串有幾個 8 bytes(64 bits) 一組,加上 qword 可得最後一組的位址 題目中提到: `若輸入的字串是一個有效的 UTF-8 序列,則計算其中的後續位元組數量,並將此數字從總字串長度中減去,即可確定字元數量。` 後續位元組的位原模式如題目所述為 `not bit6 and bit7` 因此接著跑 for 迴圈一次處理 64 個位元,計算想求的後續位元組數量:(如測驗題目說明) 1. 輸入的位元組 1. 反向運算 (not bit6) 1. 隔離反向的第 6 個位元 1. 對第 6 個位元,向左位移 1 個位元 (移至第 7 個位元的位置) 1. 進行 not bit6 and bit7 1. 以 population count 指令計算,即 popcount(t4) 再來將算出來的數字 count 用總字串長度減去 而須注意的是目前只做了可以 64 bits 一組處理的部分,也就是這裡的總長度不應該用原本字串的總長度,而是要先用 len / 8 計算做了幾組,再乘 8 (1 << 3),算出目前已計算的總長度,再減去 count 最後計算有沒有多出來不能 64 bits 一組一起算的部分,以 len & 7 判斷,也就是 len % 8,若有的話再用 count_utf8 計算並加進 count,其長度為 len & 7,也就是以 8 bytes 為一組剩餘的長度;若沒有的話就加 0。 ### 測驗 4 判定 16 位元無號整數是否符合特定樣式 (pattern): ```c #include <stdint.h> #include <stdbool.h> 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) => 1000 0000 0000 0000 pattern: c000 (49152) => 1100 0000 0000 0000 pattern: e000 (57344) => 1110 0000 0000 0000 pattern: f000 (61440) => 1111 0000 0000 0000 pattern: f800 (63488) => 1111 1000 0000 0000 pattern: fc00 (64512) => 1111 1100 0000 0000 pattern: fe00 (65024) => 1111 1110 0000 0000 pattern: ff00 (65280) => 1111 1111 0000 0000 pattern: ff80 (65408) => 1111 1111 1000 0000 pattern: ffc0 (65472) => 1111 1111 1100 0000 pattern: ffe0 (65504) => 1111 1111 1110 0000 pattern: fff0 (65520) => 1111 1111 1111 0000 pattern: fff8 (65528) => 1111 1111 1111 1000 pattern: fffc (65532) => 1111 1111 1111 1100 pattern: fffe (65534) => 1111 1111 1111 1110 pattern: ffff (65535) => 1111 1111 1111 1111 ``` 可觀察到從最左邊開始為連續的 1 ,接著是連續的 0 更精簡的程式碼: ```c bool is_pattern(uint16_t x) { const uint16_t n = ~x + 1; return (n ^ x) < x; } ``` 這種樣式下將 x 做 bitwise not 會將原本的 1 變成 0、0 變成 1,例如:`1111 0000 0000 0000` 變成 `0000 1111 1111 1111`,加一後變成 `0001 0000 0000 0000`,與 x 做 bitwise xor 後會將 x 最低位的 1 去掉,得到比 x 小的值。 而若非此樣式的,例如 `1101 0000 0000 0000`,非連續 1 接上 0,bitwise not 後會變成 `0010 1111 1111 1111`,加 1 後變成 `0011 0000 0000 0000`,會有兩個位數是 1 ,再做 bitwise xor 會變成 `1110 0000 0000 0000` 反而會進位。 而 1 接上非連續 0 的情況,例如 `1111 0001 0000 0000`,bitwise not 再加一後會變成 `0000 1111 0000 0000`,也會有不只一個位數是 0,做 bitwise 後會變成 `1111 1110 0000 0000`,數值也會比較大。 可知只有連續 1 接連續 0 的情況下作這些操作後可去掉最低位的 1 ,得到比原本更小的數值。