---
tags: linux2023
---
# 2023q1 Homework2 (quiz2)
contributed by < [chiangkd](https://github.com/chiangkd) >
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
CPU family: 6
Model: 158
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 9
CPU max MHz: 3800.0000
CPU min MHz: 800.0000
BogoMIPS: 5599.85
```
## 測驗一
**延伸問題**
- [x] 解釋程式碼原理,並用 [`__builtin_clzl`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 改寫
>`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.
- [ ] 在 Linux 核心原始程式碼找出類似的使用案例並解釋
- [x] 當上述 clz 內建函式已運用時,編譯器能否產生對應的 x86 指令?
>提示: 可執行 `cc -O2 -std=c99 -S next_pow2.c` 並觀察產生的 next_pow2.s 檔案,確認 `bsrq` 指令 (表示 “[Bit Scan Reverse](https://c9x.me/x86/html/file_module_x86_id_20.html)”)
在原程式的實作方式中,透過 binary search 的方式,與輸入值做比較,直到找到符合輸入值的 2 的冪
```c
#include <stdint.h>
static inline uint64_t pow2(uint8_t e) { return ((uint64_t)1) << e; }
uint64_t next_pow2_m1(uint64_t x)
{
uint8_t lo = 0, hi = 63;
while (lo < hi) {
uint8_t test = (lo + hi)/2;
if (x < pow2(test)) { hi = test; }
else if (pow2(test) < x) { lo = test+1; }
else { return pow2(test); }
}
return pow2(lo);
}
```
以輸入 `x = 7` 舉例,`lo` 和 `hi` 會經過以下迭代
| lo | hi | test |
| --- | --- | ---- |
| 0 | 63 | 31 |
| 0 | 31 | 15 |
| 0 | 15 | 7 |
| 0 | 7 | 3 |
| 0 | 3 | 1 |
| 2 | 3 | 2 |
然後觸發 `else if` 條件
```c
else if (pow2(test) < x) { lo = test+1; }
```
並回傳 `pow2(lo) = 8`,上述程式碼具有分支,以下是沒有分支的版本
```c
uint64_t next_pow2_m2(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;
}
```
考慮到題目敘述給定 64 位元數值,故上述程式碼必須將 將最高位是 1 的後面都填充為 1 ,在前 7 個 `x |= x >> 1` 操作中已將包含最高位的頭 8 個 bit 填補為 1,考慮函數功能, `AAAA` 為 `8`, `BBBB` 為 `32` 即可填補完成。
但上述沒有分支的實作中在遇到 2 的冪次方輸入時,會產生錯誤,假設輸入 `8(0b1000)`,經過上述運算過後將會回傳 `15(0b1111)+1 = 16`,而預期輸出為 `8`,故我對程式碼稍做修改,新增一個 logical and (`&&`) 判斷輸入是否為 `0` 並對修正 2 的冪次方輸入所造成的問題。
```c
uint64_t next_pow2_m2(uint64_t x)
{
x -= x && 1; /* if x != 0, x = x-1, for 2^k,it will borrow 1 bit from the top */
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;
}
```
### 用 `builtin_clzl` 改寫
[`__builtin_clzl`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 回傳有幾個 leading 0,當輸入為 0 時,輸出 undefined。
有幾個 leading 0 代表有值第一個 1 出現在 `64 - __builting_clzl(x)`,也就是說最接近的大於等於 2 的冪的值為 `1 << (64 - __builting_clzl(x))`
將上述的 branch less 用此函式可改寫為
```c
uint64_t next_pow2_m3(uint64_t x)
{
x -= x && 1;
return 1 << (64 - __builtin_clzl(x));
}
```
同樣的考慮到上述提到的輸入為 2 的冪次方以及輸入為 0 的問題,不過當前版本在輸入 `x` 為 0 或 1 時會造成 `__builtin_clzl(0)` ,為 `undefined` ,且預期輸出應為 `1`,故可將函式改寫為
```c
uint64_t next_pow2_m3(uint64_t x)
{
x -= x && 1;
return x ? 1 << (64 - __builtin_clzl(x)) : 1;
}
```
### 編譯器能否產生對應的 x86 指令
```shell
gcc -O2 -std=c99 -S next_pow2.c
```
函式名稱為 `next_pow2_m3`,產生的對應 `x86` 指令
```shell
next_pow2_m3:
.LFB16:
.cfi_startproc
endbr64
cmpq $1, %rdi
movl $1, %eax
adcq $-1, %rdi
testq %rdi, %rdi
je .L10
bsrq %rdi, %rdi
movl $64, %ecx
xorq $63, %rdi
subl %edi, %ecx
sall %cl, %eax
cltq
.L10:
ret
.cfi_endproc
```
有產生對應的 `bsr` 指令,其中後綴可以為 `b`、`w`、`l`、`q` 分別代表 8 位,16 位,32 位,64 位。因本函式處理 `uint64_t` ,所以會看到大量的 `q` 後綴
## 在 Linux 核心原始程式碼找出類似的使用案例並解釋
>TODO
---
## 測驗二
**延伸問題**
- [x] 解釋程式碼運作原理
- [ ] 嘗試使用 [`__builtin_{clz,ctz,ffs}`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 改寫,並改進 $10^9+7$ 的運算
```c
int concatenatedBinary(int n){
const int M = 1e9 + 7;
int len = 0; /* bit length to be shift */
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 ~ n 轉換程 binary 表示後的串接,先用 `n=4` 推演看看
```shell
value binary
n = 1 -> 1
n = 2 -> 10
n = 3 -> 11
n = 4 -> 100
concatenated -> 11011100 = 220
```
接著搭配題目註解分析,將 rightmost 的 set bit 移除後即為 2 的冪次方,且遇到 2 的冪次時,要增加 bit 的長度,對應上述,在遇到 `n=2` 時,二進位值由 `1` 變為 `10` ,也就是在二進制下**進位**,所以這裡才會需要增加 bit 的長度,同理遇到 `n=4` 時也一樣,故 `DDDD` 就必須處理移除 rightmost set bit 這件事情,也就是 `i & (i-1)` ,這裡如果 `i` 為 2 的冪次時,若減一則會向 leftmost set bit 借位,此時取 bitwise and 若為 0 ,代表 set bit 僅有 leftmost set bit 一位,即代表 `i` 屬於 2 的冪次,需增加 `bit` 長度。
接著 `EEEE` 為將二進位資料串接的過程,故 `EEEE` 為 `ans << len`,用同樣的例子推演一次
```shell
value binary
n = 1 -> 1
n = 2 -> 10
n = 3 -> 11
n = 4 -> 100
/* iteration */
ans = 0b00000000 (len = 1)
ans = 0b00000001 (len = 1)
ans = 0b00000110 (len = 2)
ans = 0b00011011 (len = 2)
ans = 0b11011100 (len = 3)
```
### 嘗試使用 `__builtin_{clz,ctz,ffs}` 改寫並改進 $10^{9}+7$ 運算
- `__builtin_clz`: 回傳 leading 0 的數量
- `__builtin_ctz`: 回傳 trailing 0 的數量
- `__builtin_ffs`: 回傳從右邊數起第一個 1 的位置
在原方法中的 `len` 是根據是否有進位來判斷 left shift 的長度為多少,在這裡可以直接用 `32 - __builtin_clz(i)` 來判斷 left shift 長度,可改寫如下
```c
int concatenatedBinary_m2(int n){
long ans = 0;
const int M = 1e9 + 7;
for(int i = 1; i <= n; i++)
ans = (i | ans << (32 - __builtin_clz(i))) % M;
return ans;
```
- 一樣可以通過 [leetcode 1680](https://leetcode.com/problems/concatenation-of-consecutive-binary-numbers/submissions/910890142/)
文章 [Modulo 10^9+7 (1000000007)](https://www.geeksforgeeks.org/modulo-1097-1000000007/) 提到為何要進行 mod 運算。
:::warning
TODO:
暫時還沒想到 mod 該怎麼優化,感覺可以從 [modulo](https://en.wikipedia.org/wiki/Modulo) 的等價性 (identities) 下手
:::
---
## 測驗三
**延伸問題**
- [x] 解釋程式碼運作原理,比較 SWAR 和原本的實作效能落差
- [ ] 在 Linux 核心原始程式碼找出 UTF-8 和 Unicode 相關字串處理的程式碼,探討其原理,並指出可能的改進空間
```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 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;
}
```
- 編譯時發現錯誤,根據 [Precedence table](https://en.cppreference.com/w/c/language/operator_precedence) ,`+` 的優先權高於 `>>` ,根據函式功能應該要加個括號才合理。
透過 8 bytes 長度的 `qword` 將原先 1 byte 的 `char` 放進去計算,根據編碼規則, UTF-8 對於首位元組最高的兩個位元始終為 `11`,而後續位元組的最高兩個位元設定為 `10`
| ASCII | 0xxx.xxxx |
| ------- | -------------------------------------- |
| 2 bytes | 110x.xxxx 10xx.xxxx |
| 3 bytes | 1110.xxxx 10xx.xxxx 10xx.xxxx |
| 4 bytes | 111.0xxx 10xx.xxxx 10xx.xxxx 10xx.xxxx |
透過[題目](https://hackmd.io/@sysprog/linux2023-quiz2#%E6%B8%AC%E9%A9%97-3)說明的規則將特定 pattern (`not bit6 and bit7`) 提取出來後利用 `__builtin_popcount` 計算有多少 `1` (也就是符合規則的部份)
在 `for` 迴圈中處理超過 $N * 8$ bytes 的部份 (即可以放滿 `uint64_t` 的部份),後續迴圈外的部份為處理剩下的 7 bytes
在迴圈外的 `(1 << 3) * (len / 8) - count;` 是為了計算出字元的數量,將總字串長度撿到 continuation bytes (符合 pattern 部份) 數量即可拿到 8 bytes 整數倍部份的字元總數。
最後再來處理不為 8 bytes 整數倍的部份,在前一行透過 `len / 8` 取出整數倍部份, 此行則透過 `len & 7` 取出不為整數倍的部份,若不為 `0`, 則直接透過 `count_utf8` 計算並加入結果之中。
### 效能測試
引入 [cpucycles.h](https://github.com/sysprog21/lab0-c/blob/master/dudect/cpucycles.h)
測試 buffer size 為 10000,20000,50000,100000 下的 ticks
- [test code](https://github.com/chiangkd/NCKU-Linux-Kernel-Quiz/commit/f727bf0bac535f78423ac8017946e71686cce532)
| Buffer Size | w/o SWAR | w/ SWAR |
| ----------- | -------- | ------- |
| 10000 | 257978 | 39937 |
| 20000 | 515033 | 76065 |
| 50000 | 1124807 | 169976 |
| 100000 | 2323698 | 361673 |
![](https://i.imgur.com/kczTO9K.png)
可以看到有使用 SWAR 的明顯較佳。
---
## 測驗四
**延伸問題**
- [x] 解釋程式碼運作原理
- [ ] 在 Linux 核心原始程式碼找出上述 bitmask 及產生器,探討應用範疇
參見 [Data Structures in the Linux Kernel](https://0xax.gitbooks.io/linux-insides/content/DataStructures/linux-datastructures-3.html)
題目要求要找到的 pattern 為從 MSB 開始有 1 個或以上連續的 `1` ,接著剩餘的位數都是 `0` ,符合條件的 pattern 包含
```shell
pattern: 8000 (32768) 0b1000 0000 0000 0000
pattern: c000 (49152) 0b1100 0000 0000 0000
pattern: e000 (57344) 0b1110 0000 0000 0000
pattern: f000 (61440) 0b1111 0000 0000 0000
pattern: f800 (63488) 0b1111 1000 0000 0000
pattern: fc00 (64512) 0b1111 1100 0000 0000
pattern: fe00 (65024) 0b1111 1110 0000 0000
pattern: ff00 (65280) 0b1111 1111 0000 0000
pattern: ff80 (65408) 0b1111 1111 1000 0000
pattern: ffc0 (65472) 0b1111 1111 1100 0000
pattern: ffe0 (65504) 0b1111 1111 1110 0000
pattern: fff0 (65520) 0b1111 1111 1111 0000
pattern: fff8 (65528) 0b1111 1111 1111 1000
pattern: fffc (65532) 0b1111 1111 1111 1100
pattern: fffe (65534) 0b1111 1111 1111 1110
pattern: ffff (65535) 0b1111 1111 1111 1111
```
```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;
}
```
原方法在 `x > 0` 的情況下 (迴圈中),若檢測到 `x & 0x8000`(MSB) 為 `0` ,則不滿足 pattern,即檢查若符合條件,最高位元為 `0` 的情況只有在大家都為 `0`,即代表 `x=0`
```c
bool is_pattern(uint16_t x)
{
const uint16_t n = EEEE;
return (n ^ x) < FFFF;
}
```
此處回傳值為 `n ^ x < FFFF`,代表判斷是否符合 pattern 是透過比較大小,而符合 pattern 的值正好就是符合 pattern 的相同數量 `1` 中的最大值,考慮到若符合 pattern ,則其二補數為保留最靠近 LSB 的 `1`,其餘為 `0`,若不符合 pattern ,其二補數一樣會保留最靠近 LSB 的 `1` ,但在這個 `1` 的左側 bit 會進行翻轉。
若符合 pattern ,取二補數後在和本身取 `xor` 必小於原本的值,因為最靠近 LSB 的 `1` 被消除了,若不符合 pattern ,則取二補數和本身取 `xor` 必大於原本的值,因為
```shell
x = 11110111
-x = 00001001
-x ^ x = 11111110
--------------------
x = 01101111
-x = 10010001
-x ^ x = 11111110
--------------------
x = 01010101
-x = 10101011
-x ^ x = 11111110
```
最靠近 LSB 的 `1` 的左側會被 `1` 填滿,且 這個 `1` 本身會被 `xor` 成 `0`
- `EEEE` 可為 `-x` or `~x + 1`
- `FFFF` 為 `x`