# 2023q1 Homework2 (quiz2)
contributed by < `SPFishcool` >
## 測驗一
### 運作原理
程式裡 `next_pow2(x)` 要找出最接近且大於等於 `x` 的 2 的冪次方的值。
在改寫程式裡,有著幾行類似下方的程式碼。
```c
x |= x >> 1;
```
這會讓 `x` 右移一位並將 1 set 到原本的 `x` 上,例如: `00100000` 變成 `00110000`、 `00100110` 變成 `00110111`。
既然我們要找下一個 2 的冪次方的數值,那我們可以確保把第一個 1 後面都變成 1 再將 `x+1` 就能夠找到答案。
```c=
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;
```
我們可以考慮最差情況 `0x8000` 我們要 set 63 個位元 ,1 ~ 7 行已經 set 7 個 1 了,第九行又一次 set 16 個 1,因此 `AAAA` 應為 `8`,最後 `BBBB` 再 將剩下的 32 個位元 set ,因此為 `32`,最後 `CCCC` 就會是 `x+1`。
### 引入 `__builtin_clzl`
參考[6.59 Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 內容對 `__builtin_clz` 的描述
```
Built-in Function: int __builtin_ctz (unsigned int x)
Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
```
:::danger
參照 GCC Manual 這樣的第一手材料!
:notes: jserv
> 收到!學生會嘗試參照這些資訊
:::
得知這是一個系列的內建函式,`__builtin_clz` 是回傳從左左邊起第一個 1 之前 0 的個數,而 `__builtin_clzl` 最後的 `l` 代表輸入是 `long` 型態的整數。
因此,運用 `bit = 64 - __builtin_clzl(x)` 可以知道這個數值的最高位元數是多少,再回傳 `1ULL << bit` 即可。
```c
uint64_t next_pow2(uint64_t x) {
int bit = 64 - __builtin_clzl(x);
return (1ULL << (bit));
}
```
除了實作 `next_pow2` ,另外還讓使用者在執行程式時必定要輸入一個整數。
```c
int main(int argc, char *argv[]){
if (argc != 2){
printf("%s need 1 argument.\n", argv[0]);
return 0;
}
```
確保使用者數入的是一個整數,使用 `isdigit` 確認數入的是否是一個整數
```c
char *cvalue = argv[1];
while(*cvalue){
if (!isdigit(*cvalue)){
printf("%s is not an integer.\n", argv[1]);
return 0;
}
cvalue++;
}
```
`isdigit` 在編譯時出現
```bash
next_pow.c:16:10: warning: implicit declaration of function ‘isdigit’ [-Wimplicit-function-declaration]
16 | if (!isdigit(argv[1])){
| ^~~~~~~
next_pow.c:4:1: note: include ‘<ctype.h>’ or provide a declaration of ‘isdigit’
```
這是沒有 `#include` 到必要的函式庫出現的警訊,寫上 `#include<ctype.h>` 就不會出現了。
最後確定為整數就可以使用 `atoll` 將字串轉為 `long long`
```c
uint64_t input = atoll(argv[1]);
```
:::warning
在實作時注意題目的資料型態,此題的型態為 `uint64_t`( `unsigned long` )
:::
### 在 linux 核心原始程式碼中的相似案例
### 觀察 x86 指令
在經過執行以下指令編譯後
```bash
cc -O2 -std=c99 -S next_pow.c
```
觀察到有產生以下 x86 組合語言
```x86=
bsrq %rdi, %rdi
movl $64, %ecx
movl $1, %eax
xorq $63, %rdi
subl %edi, %ecx
salq %cl, %rax
```
第 1 行:`brsq` 能夠將 `%rdi` 的最高位元位置存入 `%rdi`,接下來將 `%edi`
第 4 行:`xorq` 再將 63 與 `%rdi` 做 `xor` 運算,在無號數相當於 63 - `%rdi`
這兩行相當於 `__builtin_clzl` 函式,編譯器的做法就是找出最高位元再與 63 相減得出「出現 1 的最高位元前面 0 的個數」。
## 測驗二
### 運作原理
詳細題目請見 LeetCode [1680. Concatenation of Consecutive Binary Numbers ](https://leetcode.com/problems/concatenation-of-consecutive-binary-numbers/)
此程式只有一個 for 迴圈,內部執行兩個指令
```c=
for (int i = 1; i <= n; i++) {
if (!(DDDD))
len++;
ans = (i | (EEEE)) % M;
}
```
`len` 一開始為 `0`,代表著 `i` 這個數字的 2 進位長度,所以我們可以看 2、3 行, `len++` 的時機點應該就是 2 進位長度增加的時候,也就是 $2^n$,依照 [quiz2 題目](https://hackmd.io/@sysprog/linux2023-quiz2)要求,要判斷 $2^n$ ,`DDDD` 可以填 `i & (i - 1)`。
最後第 4 行會將 `i` 存入 `ans` ,因此在存入之前需要將 `ans` 左移到可以將 `i` 放入的量,`EEEE` 就會是 `ans << len`。
### 使用 GNU 內建函式
若使用 GNU 內建的 `__buitin_clz` 可以省去判斷是否為 $2^n$ ,且 `__buitin_clz` 時間複雜度為 $O(1)$,也同時可以省去使用 if 判斷式的 branch 。
```c
for (int i = 1; i <= n; i++)
ans = (i | (ans << (32 - __builtin_clz(i)))) % M;
```
## 測驗三
### 運作原理
此題為運算出 [utf-8](https://en.wikipedia.org/wiki/UTF-8) 字元數量,原理為只要判斷出首位元組和後續位元組就可以得出 utf-8 字元數量。而後續位元組都有一個共通點:最高的兩個位元為 `10`,
| **ASCII** | Bytes 1 | Bytes 2 | Bytes 3 | Bytes 4 |
|------------|---------|---------|---------|---------|
| 1 Bytes | 0xxx xxxx |
| 2 Bytes | 110x xxxx | 10xx xxxx |
| 3 Bytes | 1110 xxxx | 10xx xxxx | 10xx xxxx |
| 4 Bytes | 1111 0xxx | 10xx xxxx | 10xx xxxx | 10xx xxxx|
```c
for (size_t i = 0; i < len; i++) {
/* -65 is 0b10111111, anything larger in two-complement's should start
* new code point.
*/
if (p[i] > -65)
counter++;
}
```
上述程式碼是把每組位元組當作一個 8 bits 表示的有號整數,其 `-65` (`0b10111111`) 為判斷門檻,一旦小於 `-65` 的值其 2 的補數必為 `10` 開頭。
而 SWAR 版本的程式碼將 8 個位元組(8x8 bits)組合在一起並運用特殊的 Mask 來計算後續位元組的數量。
而原本 `len` 為位元組數量,經過 `len >> 3` 就變成 `uint64_t *` 的陣列長度。
```c
const uint64_t *qword = (const uint64_t *) buf;
const uint64_t *end = qword + len >> 3;
```
迴圈內就是一系列的操作,其操作會使 `10xx xxxx` 的位元組第 7 個位元為 1 ,其餘都為 0,最後再使用 `__builtin_popcountll(t4)` 計算 1 的數量就是後續位元組的數量。
```c=
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);
}
```
操作原理如下:
第 3 行:將 64 位元取 1's 補數
第 4 行:`0x04040404040404040llu` 只會將每一位元組的第 6 位元保留下來,其他都會 clear 成 0。
第 5 行:會將第 4 行的結果進一位。
第 6 行:會將 `t0` 與 `t3` 進行比較。
在最後 `__builtin_popcountll(t4)` 算出後續位元組的數量,再將總長度 - 算出來的數量。
```c
count = (1 << AAAA) * (len / 8) - count;
```
在這段程式碼其實故意寫得較為冗長,`(1 << AAAA) * (len / 8)` 其實就是總長度 `len`。所以 `AAAA` 應是 `3`。
最後位元組數量可能不整除 8 ,所以最後可能會有不滿 8 個的位元組。
最後使用一般策略的 `count_utf8()` 來計算這些字元數量。
```c
count += (len & BBBB) ? count_utf8((const char *) end, len & CCCC) : DDDD
```
`(len & BBBB) ?` 是要取出後半段不滿 8 個的位元組,所以 `BBBB` 應是 `7`(`0b111`),如果 true,就叫算接下來的部分,因此 `count_utf8()` 裡面 `end` 已經指向尚未處理完的第一個位元組,`len & CCCC` 是還沒處理的長度,因此 `CCCC` 也是 `7`。
如果 false,就不用做任何事了,因此 `DDDD` 為 `0`。
## 測驗四
### 運作原理
可以由符合原本的程式碼來看,符合特定樣式的 1-bit 都是從最高位元由高到低連續排列,只要中間有 0 就不是我們要的樣式。
改寫過的程式碼先將 `n` 設為 `-x`,依照 2's component `x` 從 LSB 算起遇到第一個 `1` 接下來才要做 NOT 運算,例如: `11110000` 2's component is `00010000`,接下來 return 一個邏輯判斷式,`n ^ x` 的結果會少一個 `1`,例如:`11110000 ^ 00010000 = 11100000`,這時候如果 `1` 的中間有任何一個 `0` 便會小於此數,不然都會大於此數(多一個 `1`)。
```c
const uint16_t n = EEEE;
return (n ^ x) < FFFF;
```
所以 `EEEE` 為 `-x`,`FFFF` 為 `x`。