# 2023q1 Homework2 (quiz2) contributed by < `chiacyu` > # 測驗 `1` 考慮 `next_pow2` 可針對給定無號 `64` 位元數值 `x`,找出最接近且大於等於 `2` 的冪的值 ```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); } ``` 程式碼其假設的情境是在 Big-Endian 的情境下,是找出最接近且大於等於等於 `2` 的冪的值。 假設 `x = 0x70000000` ,以二進位來表示則為 `0x0111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000` ,經過一系列運算後則為: ```c uint64_t next_pow2(uint64_t x) { //0x0111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 x |= x >> 1; //0x0111 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 x |= x >> 1; //0x0111 1100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 x |= x >> 1; //0x0111 1110 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 x |= x >> 1; //0x0111 1111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 x |= x >> 1; //0x0111 1111 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 x |= x >> 1; //0x0111 1111 1100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 x |= x >> 1; //0x0111 1111 1110 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 x |= x >> 8; //0x0111 1111 1111 1111 1110 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 x |= x >> 16; //0x0111 1111 1111 1111 1111 1111 1111 1111 1110 0000 0000 0000 0000 0000 0000 0000 x |= x >> 32; //0x0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 return (x+1); ////0x1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 } ``` ## `__builtin_clzl` 改寫 從 [`__builtin_clzl`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 可以看到回傳值為 `Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.` 舉例來說假設輸入值為 `16`, 以 `64bit` 的數值系統,其二進位的表達式則為 `00000000 00000000 00000000 00000000 00000000 00000000 00000000 00010000`,回傳值則為 `59`,因此可以改寫為以下 ```c uint64_t next_pow2(uint64_t x) { // If x is 0, the result is undefined. if (x == 0) return 1; int leadin_z_count = __builtin_clzl(x); return (1 << (64 - leadin_z_count)) } ``` 這邊需要注意的地方是 `__builtin_clzl()` 在輸入值為 `0 ` 的時候的結果是 `undefined` 因此需要特別注意。 ## Linux 核心使用案例 可以看到在 [log2.h](https://github.com/torvalds/linux/blob/master/include/linux/log2.h) 可以看到有相關的實作,如: ```c unsigned long __roundup_pow_of_two(unsigned long n) { return 1UL << fls_long(n - 1); } ``` :::warning `lab0-c` 的程式碼 [log2_lshift16.h](https://github.com/sysprog21/lab0-c/blob/master/log2_lshift16.h) 則有另一項實作,應用於 [shannon_entropy.c](https://github.com/sysprog21/lab0-c/blob/master/shannon_entropy.c),可一併對照討論。 :notes: jserv ::: ## 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 (!(i & (i-1))) len++; ans = (i | (ans << len)) % M; } return ans; } ``` 觀察數值可以看到當數值為 `2` 的冪,`bit` 的位數則會加一,例如: ```c 0 //0 1 //1 2 //10 3 //11 4 //100 ``` 所以可以透過 `!(i & (i-1))` 來去判斷 `i` 是否為 2 的冪。 每次的迭代須將結果往 `most significant bit` 位移 `len bit`。 假設輸入值為 2, 迴圈由 `i = 1` 開始。 ```c i = 1 // ans = 1 i = 2 // ans = 110 ``` ## `__builtin_{clz,ctz,ffs}` 改寫 從範例可以知道 `ans` 在每一次的迭代需要預留下一個 `i` 值的空間。 例如 `i = 2` 以二進位來表示為 `10` 因此 `ans` 需要往左位移兩個`bit`來容納`10`, 因此我們可以善用`built in function`來改寫為 ```c int concatenatedBinary(int n) { long ans = 1; for (int i = 2; i <= n; i++) { ans = (i | (ans << (64 - __builtin_clzl(i)))); } return ans; } ``` 透過 `__builtin_clzl` 來知道 `i` 往最高位元數有幾個 `0` , 而 `64 - __builtin_clzl(i)` 就可以知道 `i` 所佔位元數。例如 `__builtin_clzl(2)` 會回傳 `62`, 64 - 62 = 2,就剛好是`10`所佔用的位元數。 --- # 測驗 `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; 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 & 0x7) ? count_utf8((const char *) end, len & 0x7) : 0; return count; } ``` 這邊花了一點時間才了解其中的技巧,需要注意的是原本傳入的 `buf` 的資料結構是`char *`,而每一個 `char` 只佔用一個 `byte`,在 `* qword` 這邊做強制轉型成 `unsigned int 64` 佔用8個`byte`。 因為做了資料轉型,原先的 `len` 也要做出對應的調整。 `>> 3` 可以當成除以 `8`。 假設原先`len = 8` , 需要從`buf[0].....buf[7]`才可以看完所有的資料。 轉型之後只需 `qword[0]` 便可以處理完。 這邊有趣的地方在於 `count` 在不同的 `function scope` 代表不同的意思。 ```c count += __builtin_popcountll(t4); ``` - count : number of continuation bytes ```c count = (1 << 3) * (len / 8) - count; ``` - count total bytes - continuation bytes !!!missing a colon :, confusing to some people. by Urbaner3 ## Linux UTF-8 和 Unicode 討論 最後有個有趣的技巧,當 `len` 不為`8`的倍數時,需要而外處理剩餘的部分。 例如: `len = 9`,除完之後還會剩下 `1`,因此剩餘的部分用 `count_utf8()` 方法去處理。 對 `8` 在 `mod` 運算可以用對 `0x7` 做 `and` 運算。 --- # 測驗 `4` ```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; } bool is_pattern(uint16_t x) { const uint16_t n = ~x + 1; return (n ^ x) < x; } ``` 從題目的要求與敘述,該程式所驗證於一個 16 位元的數值,從 `MSB` 開始是否為連續的 `1`。 ```c //MSB LSB 1000 0000 0000 0000 1100 0000 0000 0000 1110 0000 0000 0000 ... ... ... 1111 1111 1111 1111 ``` 但如果是從 `LSB` 開始即便中間無間隔任何 0 ,也不符合要求,例如: ```c 0000 0000 0000 0001 0000 0000 0000 0011 ... ... ... ``` 這樣子是不符合要求的。 這邊的做法是先將 `x` 值做 `~` 運算加上一,如果是有號數`(signed number)`等同於取負值。 在與原先的數字做 `exlusive or` ,注意這邊是 `uint16_t` 因此數字中`1`的位數越多則數值越大。 因此若是不符合我們要求的 `pattern` 在做完運算後反而會比遠先的數值更大。 因此可以利用此特性來作為條件判斷。 ## Linux bitmask 探討應用