# 2023q1 Homework2 (quiz2)
contributed by < `yanjiew1` >
[作業說明](https://hackmd.io/@sysprog/H143OpNCo)
## 實驗環境
```bash
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
$ 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) i5-8250U CPU @ 1.60GHz
CPU family: 6
Model: 142
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 10
CPU max MHz: 3400.0000
CPU min MHz: 400.0000
BogoMIPS: 3600.00
```
## 測驗 1
### 程式說明
一開始 `x |= x >> 1;` 中在此程式出現 7 次。即這段 64 bits 數值中,每一個 1 向右擴展 7 個 1 。
```
00...01000000000...000
|
v
00...01111111100...000
```
`x |= x >> 8;` ,因為前面的 `x |= x >> 1;` ,至此 `x` 裡的 1 已向右擴展 7 個,故有 8 個連續的 1,透過再跟 `x >> 8` 進行 or 運算,會把這些連續的 1 ,再向右擴展 8 個 1。 `AAAA` 填入 `8`
`x |= x >> 16;` 會把 16 個連續的 1 再向右擴展 16 個,會讓 `x` 變成有至少 32 個連續的 1 。
`x |= x >> 32;` 會把 32 個連續的 1 再向右擴展至 64 個,會讓 `x` 變成有至少 64 個連續的 1 。 `BBBB` 填入 `32` 。
最後會讓 `x` 中最高位的 `1` 擴展至最低位,變成 `00...01111111111...111` 。
因為這個函式的目的是要找下一個 2 的冪 。只要再加上 1 ,即會讓這些 1 進位,變成下一個 2 的冪。故最後 `return x + 1;`。 `CCCC` 填入 `x + 1`
另外這個函式其實不完全正確。題目說「考慮 next_pow2 可針對給定無號 64 位元數值 x,找出最接近且**大於等於** 2 的冪的值」,故應該要在函式的開頭加入 `x--;` ,先把 `x` 減去 1 ,這樣才能讓原本就是 2 的冪的數值維持一致。
另外此作法,可簡化為:
```c
uint64_t next_pow2(uint64_t x)
{
x--;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
x |= x >> 32;
return x + 1;
}
```
### 用 `__builtin_clzll` 改寫
特別注意的是 `__builtin_clzll` 傳入的值若為 `0` ,則為未定義行為,會產生不可預期的結果。故一開始的 `if` 條件判斷式即排除此狀況。
```c
uint64_t next_pow2(uint64_t x)
{
if (x <= 1)
return 1;
return 1 << (64 - __builtin_clzll(x - 1));
}
```
用 Compiler Explorer 來實驗,[連結在此](https://godbolt.org/z/bWGnvPvEh) ,的確有出現 `bsrq` 指令。
```
next_pow2:
movl $1, %eax
cmpq $1, %rdi
jbe .L1
subq $1, %rdi
movl $64, %ecx
bsrq %rdi, %rdi
xorq $63, %rdi
subl %edi, %ecx
sall %cl, %eax
cltq
.L1:
ret
```
### Linux 核心範例
Linux 有關 2 的冪相關的程式在這裡 [linux/include/linux/log2.h](https://github.com/torvalds/linux/blob/master/include/linux/log2.h) 。最接近上述的是 `__roundup_pow_of_two` 這個函式。但跟測驗的 `next_pow2` 不太一樣是,測驗是找「下一個 2 的冪」,也就是 4 的下一個 2 的冪會是 8 ,但 Linux kernel 裡的 `__roundup_pow_of_two` 是直接取大於等於輸入值且最接近輸入值的 2 的冪。
在 [elixir.bootlin.com](https://elixir.bootlin.com/) 這個網站裡搜尋使用到 [`__roundup_pow_of_two`](https://elixir.bootlin.com/linux/latest/A/ident/__roundup_pow_of_two) 的程式碼發現在許多的裝置驅動程式內用到。
```
Defined in 2 files as a function:
include/linux/log2.h, line 55 (as a function)
tools/include/linux/log2.h, line 47 (as a function)
Referenced in 16 files:
drivers/clk/clk-divider.c
line 226
line 244
line 282
drivers/clk/sunxi/clk-sunxi.c, line 314
drivers/clk/zynqmp/divider.c, line 59
drivers/infiniband/hw/hfi1/chip.c
line 14321
line 14324
drivers/infiniband/hw/hfi1/init.c, line 433
drivers/iommu/intel/dmar.c, line 1544
drivers/iommu/intel/iommu.c, line 1505
drivers/iommu/intel/irq_remapping.c, line 118
drivers/iommu/intel/svm.c, line 201
drivers/net/ipa/gsi_trans.c, line 150
drivers/pci/msi/msi.c, line 303
include/linux/log2.h, line 180
net/mac80211/cfg.c, line 1155
net/sunrpc/xprtrdma/verbs.c, line 857
tools/include/linux/log2.h, line 157
tools/perf/builtin-sched.c
line 1904
line 2290
```
## 測驗 2
### 程式說明
在這個 `for` 迴圈中,要串接由 1 到 n 的二進位數值至 `ans` 。其中 `len` 為目前 `i` 二進位數值所需要的位元數量。
```c
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;
}
```
其中,每次 `i` 進位時,即 `i` 為 2 的冪時,長度會多一個位元 ,故可用 `i & (i - 1)` 來判斷是否為 2 冪。
- 若 `i` 為 2 的冪,`i` 只會有一個位元是 1 ,而 `i - 1` 會讓這個位元變為 0 ,在這個位元的右邊變成全為 1 ,此時 `i & (i - 1)` 為 0
- 若 `i` 不為 2 的冪,則 `i` 會有多個 bit 是 1 ,那麼 `i - 1` 只會讓最右邊是 1 的位元變為 0 ,並讓此位元的右邊其他的位元變為 1 , `i & (i - 1)` 就會讓 `i` 最右邊是 1 的位元變成 0 。
`ans = (i | (EEEE)) % M;` ,會把現有的 `ans` 先左移 `len` 個位元空出空間後,再把 `i` 串加上去。故可用 `ans << len` 來進行左移。
`ans` 雖然是模 M 後的結果,但仍不影響操作。因為 `<< len` 實際上是乘上 $2^{\text{len}}$ ,而 `|` 雖然為 or 運算,但因為 `ans` 左移後空下來的位置位元是 0 ,故在這裡其作用是加法。而對 `ans` 先乘加後再取餘數與先對 `ans` 取餘數再乘加,結果是一樣的。
### 使用 `__builtin_clz` 改寫
可以直接使用 `32 - __builtin_clz(n)` 來算出 `i` 需要幾個位元。
```c
int concatenatedBinary(int n)
{
const int M = 1e9 + 7;
/* use long here as it potentially could overflow for int */
long ans = 0;
for (int i = 1; i <= n; i++) {
int len = 32 - __builtin_clz(i);
ans = (i | (ans << len)) % M;
}
return ans;
}
```
目前還沒想到怎麼改進 `% (1e9 + 7)` ,但應該可以透過減去某二個數字相乘來達到同樣的效果
## 測驗 3
此題介紹了 SWAR 軟體最佳化的技巧,以及它如何運用在計算 UTF-8 字串中,字的數目。詳細 bitwise 的操作可以看 [quiz2 題目說明](https://hackmd.io/@sysprog/linux2023-quiz2)
此題 `swar_count_utf8` 函式,在 `for` 迴圈內,每次讀進 8 個位元組。透過 bitwise 操作,讓整個 64bit 的無號整數,值為 1 的位元數量等同於此 8 個位元組中,屬於 UTF-8 中後續位元組 (continuation byte(s)) 的數量。
```c
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` 會存放字串中, `(len & ~7)` 後續位元組 (continuation byte(s)) 的數量。
接下來就是直接把 `(len & ~7)` 減去 `count` ,則得到了前 `(len & ~7)` 中, UTF-8 中字元的個數。故這裡 `AAAA` 可填入 `3` 。
```c
count = (1 << AAAA) * (len / 8) - count;
```
再來要處理最後的 `(len & 7)` 個位元組。最後一行判斷 `(len & 7)` 是否還有位元組未處理,若還有則呼叫 `count_utf8` 來處理。故這裡 `BBBB` 和 `CCCC` 可填入 7
```c
count += (len & BBBB) ? count_utf8((const char *) end, len & CCCC) : DDDD;
```
### 效能比較
[測試程式](https://github.com/yanjiew1/linux23q1-quiz2/blob/main/utf-8.c)
我把二種實作放進我的測試程式中,用下列命令來編譯
```bash
$ gcc -O2 utf-8.c
```
測得結果為:
| 實作 | 花費時間 (cycles) |
|:--------------- | -----------------:|
| count_utf8 | 54378.65 |
| swar_count_utf8 | 30444.72 |
`swar_count_utf8` 可得到較高的效能。
若用 `gcc -O3 utf-8.c` 來編譯,則 `count_utf8` 會比較快。[此為](https://godbolt.org/z/3KMj5j7b6)編譯後的組合語言 `count_utf8` ,用到很多 SIMD 指令且 `swar_count_utf8` 沒有利用 SIMD 指令(可能太複雜讓 GCC 不好進行最佳化),這或許就是 `count_utf8` 比較快的關鍵。
### Linux 核心的 UTF-8 和 Unicode
簡單搜尋了 Linux UTF-8 的實作,有以下位置
- [`fs/nls/nls_base.c`](https://github.com/torvalds/linux/blob/master/fs/nls/nls_base.c)
- [`fs/nls/nls_utf8.c`](https://github.com/torvalds/linux/blob/master/fs/nls/nls_utf8.c)
- [`fs/unicode`](https://github.com/torvalds/linux/tree/master/fs/unicode)
不過沒有看到核心有用到計算 UTF-8 中 Unicode 字元數量的部份。比較接近的是在 [`fs/unicode/utf8-norm.c`](https://github.com/torvalds/linux/blob/master/fs/unicode/utf8-norm.c) 中的 `utf8nlen` ,但功能仍然不同 。
## 測驗 4
觀察題目 pattern ,可以知道原始程式功能是判斷 16 bits 無號整數,其位元的組成是否符合正規表示式 `1+0*` 。
先觀察 `-x ^ x` 的特性。 `-x` 是取 2 的補數,即 `~x + 1` 。 當我們對一個數字取 2 的補數時,可以觀察到,此動作會把原本數字最右邊為 1 的位元其左邊所有位元反轉,例如:
```
1011001100
=> 0100110100
```
又從 xor 真值表可知,若二位元相異,則其值為 1 反之為 0 。故 `-x ^ x` 中, 輸出值中, `x` 最右邊為 1 的位元其所有左邊位元會是 1 ,其餘為 0 ,即:
```
1011001100
=> 1111111000
```
接下來考慮二個 case :
- 當 `x` 符合正規表示式 `1+0*` 時, `-x ^ x` 會恰好讓開頭 1 的位元數少一個,即 `11110000 => 11100000` ,即 `-x ^ x < x` 。
- 當 `x` 不符合 `1+0*` 時,雖然輸出值中,對應回 `x` 中最右邊為 1 的位元所在位置變成 0 ,但是最右邊 1 的所有左邊位元都變成 1 ,例 `1010100 => 1111000` 故 `-x ^ x > x` 。
回到此題,跟據上面的觀察,我們可以填入 EEEE = `-x` , FFFF = `x` 。
```c
bool is_pattern(uint16_t x)
{
const uint16_t n = EEEE;
return (n ^ x) < FFFF;
}
```
### Linux 核心的應用