# 2023q1 Homework3 (fibdrv)
contributed by <`gaberplaysgame`>
## 開發環境
```shell
$ 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): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: 11th Gen Intel(R) Core(TM) i5-11400 @ 2.60GHz
CPU family: 6
Model: 167
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 1
CPU max MHz: 4400.0000
CPU min MHz: 800.0000
BogoMIPS: 5184.00
```
## 研讀費氏數列相關材料並摘錄關鍵手法
### 手法
#### 遞迴法
利用 $fib(n+2)=fib(n)+fib(n+1), fib(0)=0, fib(1)=1$ 的定義最容易得到的方法,雖然只要進行 $n$ 次遞迴,但實際時間複雜度並不等於 $O(n)$,因而費時。
「什麼是費氏數列」系列影片有用到一些方法來改進 python 內的費氏數列運算。
- [PART 2](https://www.youtube.com/watch?v=TA0Dpx0LOeY) 有提到 python 內使用 lru_cache 來存已經計算過的數值的方法。
- [PART 3](https://www.youtube.com/watch?v=WyDn6wiwW74) 使用 tail recursive 的方法,將尾數兩個的數值傳給下一層的函數來避免重複計算。
- [PART 4](https://www.youtube.com/watch?v=iCSNHH45EeI) 使用迭代的方式來計算,程式碼與[費氏數列分析](https://hackmd.io/@sysprog/fibonacci-number)的基本程式碼一致,雖然能夠處理大數運算,但根據分析所言利用 timeit 去測時可以發現執行時間隨數量級而變長。
#### Binet’s Formula (公式解)
費氏數列(以下用 $fib(n)$ 代替)有以下公式解:$fib(n) = \frac{1}{\sqrt{5}}(\frac{1+\sqrt{5}}{2})^n-\frac{1}{\sqrt{5}}(\frac{1-\sqrt{5}}{2})^n$
雖然可以將遞迴呼叫次數降到 $O(1)$,但是由於浮點數精度的問題,在進行計算 $\sqrt5$ 時會導致誤差放大。引用費氏數列分析的文獻,可得知公式解其實沒有比遞迴法來的快:
>$√5$ 可以用牛頓法來算,假設每個 $n$ bit 除法需要 $n^2$ 個 bit 運算,但每一輪只需要留下前面 $Nn+logn$ 個 bit. 整體上說來需要的 bit 運算數中最大宗的大約是 $logn×(Nn+logn)2$, 再加上一些較不顯著的運算 —— **其實沒有明顯地比反覆加法快**。
#### Vorobev 等式
當 $n>=1$ 時,存在特性 $f(n+k)=f(n−1)×f(k)+f(n)×f(k+1)$,若將 $n=k$ 代入並經過一些換算可得 $f(2n)=2×f(n+1)×f(n)−(f(n))^2$,遞迴次數為 $O(logn)$,但執行時間則並非上值。
[Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling) 原則上利用這個等式求值。
- $f(2n)=2×f(n+1)×f(n)−(f(n))^2=f(n)[2f(n+1)-f(n)]$
- $f(2n+1)=f(n+1)^2+f(n)^2$
#### 配合 GNU 函式庫
GNU 的 [GMP](https://gmplib.org/) 與 [MPFR](https://www.mpfr.org/) 函式庫可以將記憶體拿來當作快取,以進行接近於無限位的小數運算。經過運算對比可以發現再大數上公式解不會比 Vorobev 等式還來的快
### [clz / ctz](https://en.wikipedia.org/wiki/Find_first_set) 一類的指令對 Fibonacci 數運算的幫助
參考作業描述中關於費波那契數列以 Fast Doubling 實作的虛擬碼,新建測試檔案 fibonacci.c,內包含 `fib()` 與 `fast_fib()`:
```c
uint64_t fib(uint64_t n)
{
if (!n)
return 0;
else if (n <= 2)
return 1;
return fib(n-1) + fib(n-2);
}
uint64_t fast_fib(uint64_t n)
{
if (!n)
return 0;
else if (n <= 2)
return 1;
uint64_t a = 0, b = 1;
for (int i = 63 - __builtin_clzll(n), j = 1UL << i;
i >= 0;
--i, j >>= 1
) {
uint64_t t1 = a * (2 * b - a); //fib(2a)
uint64_t t2 = b * b + a * a; //fib(2a+1)
if (n & j) {
a = t2;
b = t1 + t2;
} else {
a = t1;
b = t2;
}
}
return a;
}
```
`fast_fib()` 支援無號 64 位元的費波那契數運算,比使用遞迴法的 `fib()` 速度更快且可以在短時間內達成 $n > 50$ 的運算。(尚未支持超過 $2^{64}$ 的大數運算)
這邊利用 quiz2 提到的 `__builtin_clz` 變體函式來去快速取得有效位元個數,並利用變數 `j` 來對 `n` 做 bitmask 來判斷當前的位元為 1 或 0。
if-else 區塊透過進一步的 bitwise 可以改寫為:
```c
int64_t mask = !(n & j) - 1; // 結果會是 0 (0x00000000) 或 -1 (0xFFFFFFFF)
a = (t2 & mask) + (t1 & ~mask);
b = t2 + (t1 & mask);
```
## 改進大數運算
參考 [Small/Short String Optimization](https://github.com/AdrianHuang/fibdrv/tree/big_number) 進行改寫,利用字串的方式來儲存數值。若使用長度為 128 的字串可使能正確計算的數值來到 $fib(500)$ 。
```c
void bn_add (char *n1, char *n2, char *n) {
int it, digit = 0, carry = 0;
int size_n1 = strlen(n1), size_n2 = strlen(n2);
// 這邊預設 n-1 的長度 >= n-2,若沒有預設需要做額外處理
for (it = 0; it < size_n2; it++) {
digit = (n1[it] - '0') + (n2[it] - '0') + carry;
n[it] = digit % 10 + '0';
carry = digit / 10;
}
for (it = size_n2; it < size_n1; it++) {
digit = (n1[it] - '0') + carry;
n[it] = digit % 10 + '0';
carry = digit / 10;
}
if (carry)
n[it] = carry + '0';
n[it+1] = '\0';
}
```
## 效能測試
參考 [yanjiew 的筆記](https://hackmd.io/@yanjiew/linux2023q1-fibdrv#%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7%E8%A8%AD%E5%AE%9A%E8%88%87%E6%B8%AC%E8%A9%A6),利用 static 變數儲存 ktime 數值並利用 write 回傳,並改寫 client.c 使其輸出結果至 out.txt。
```c
static inline long long clock_gettime_ns()
{
struct timespec t;
clock_gettime(CLOCK_REALTIME, &t);
return t.tv_sec * 1e9 + t.tv_nsec;
}
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
long long temp = clock_gettime_ns();
sz = read(fd, buf, sizeof(buf));
long long utime = clock_gettime_ns() - temp;
buf[sz] = 0;
printf("Reading from " FIB_DEV
" at offset %d, returned the sequence "
"%s.\n",
i, buf);
long long ktime = write(fd, write_buf, sizeof(write_buf));
printf("The kernel time from fib function is %lld.\n", kt);
}
```
```shell
> make
> sudo insmod ./fibdrv.ko
> sudo ./client > out.txt
> gnuplot scripts/gnuplot.gp
> eog plot.png
```
利用 GNUplot 繪製折線圖可以得到下圖,`ktime` 的數值大致隨著 `offset` 變多而往上升,且有一些極端案例存在。
![](https://i.imgur.com/kQeUj1Y.png)
將範圍放大到 500 可以得到下圖,但重複測試可以發現每次繪製的圖表落差巨大,需要排除干擾效能分析的因素:
![](https://i.imgur.com/9jPTgkY.png)
### 將行程固定在特定的 CPU 中執行
修改 /etc/default/grub 檔案,修改為 `GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=11"`,指定 CPU 11 不會被排程器排程。並利用 `sudo update-grub` 儲存設定。
```shell
> taskset -cp 1
pid 1 目前的近似者清單:0-10
```
執行上述指令可發現修改成功。程式改以 `sudo taskset -c 11 ./client > out.txt` 執行(有多次執行),可以看到折線變得稍微平緩,但在 offset = 420 左右仍有極端峰值。
![](https://i.imgur.com/ObQ5vBl.png)
### 排除干擾效能分析的因素
參考[說明文件](https://hackmd.io/@sysprog/linux2023-fibdrv/%252F%2540sysprog%252Flinux2023-fibdrv-c)的提示,將所有指令皆執行過後再次測量時間,可以發現原有的極端峰值被移除,但關閉 SMT 後導致原有設定的 CPU 11 不能使用,再次檢查 taskset -cp 1 得知近似者僅有 0~5,故使用 CPU 5 執行。
```shell
> sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
> sudo sh scripts/performance.sh
> sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
> sudo sh -c "echo off > /sys/devices/system/cpu/smt/control"
> taskset -cp 1
pid 1 目前的近似者清單:0-5
```
![](https://i.imgur.com/VmmSovC.png)
## 加速大數運算
### 紀錄認知與疑惑
遞迴法的 Fast Doubling 為使用 Top-down 實作,對遞迴的步驟進行剖析可發現有重複計算,為改進效能而改使用 Bottom-up Fast Doubling,即上方(`fast_fib()`)所設計的版本。
範例所使用的 bn 結構體以矩陣的方式實作,配合 size 與 sign 兩數值來紀錄矩陣的長度與數的符號。
- 加減法實作較為簡單,利用矩陣的相同欄的值相加再計算進位。
- 乘法實作中使用長乘法,以並改以 unsigned long long 儲存計算結果避免乘法溢出。
- bn 結構體可以被看成 $a=\sum\limits_{x = 0}^{size-1}{2^{32x}}number_{i}$,故乘法的最大 $size = a_{size}+b_{size} + 1$
- 利用雙層 for 迴圈來計算乘法數值,並用 `bn_mult_add` 來對 $number_{i+j}$ 與 $number_{i+j+1}$ 進行結果相加。
- **疑惑**:`bn_mult_add` 的計算值到 $i = c_{size}-1$ 才結束運算,但根據下方 `x >>= 32` 與 unsigned long long 為 64 位元可知執行兩次就會結束運算,若加上 carry 最多三次,那中止條件為何不改為 `i < offset+3`,是為了避免執行 `c->number[i]` 時出現 segfault 嗎?
- bn 結構體轉換成字串由於會需要經過二進位至十進位的轉換,反而比較複雜,這邊列舉一些疑惑的點:
- 長度 len 以 `8*sizeof(int)*size`取得,為何不直接利用 32*size 而要經過 sizeof(int) 運算?
- 原本的 len 運算是位元數 / 3.322,這邊除以 3 可以保證 len 一定比實際長度大,且後面會運用到 `memmove` 來去處理最後產出長度。
- carry 是得出的特定位元,只會是 1 或 0。由於十進位需要 $carry\times 2^{nth}$ 轉換才能得出正確數值,但程式碼中沒有看見,只有看見對於溢位的操作運算,所以看不懂是如何轉成十進位的。
## 自我檢查清單
- [x] 研讀上述 ==Linux 效能分析的提示== 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 $\to$ 從中也該理解為何不希望在虛擬機器中進行實驗;
- [x] 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 [clz / ctz](https://en.wikipedia.org/wiki/Find_first_set) 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說
- [x] 複習 C 語言 [數值系統](https://hackmd.io/@sysprog/c-numerics) 和 [bitwise operation](https://hackmd.io/@sysprog/c-bitwise),思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本;
- [x] 學習指出針對大數運算的加速運算和縮減記憶體操作成本的舉措,紀錄你的認知和疑惑
- [ ] 注意到 `fibdrv.c` 存在著 `DEFINE_MUTEX`, `mutex_trylock`, `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 [POSIX Thread](https://en.wikipedia.org/wiki/POSIX_Threads) 的程式碼來確認。 $\to$ 搭配閱讀〈[並行和多執行緒程式設計](https://hackmd.io/@sysprog/concurrency)〉
- [x] 詳閱第 2 和第 3 週所有教材及作業描述 [(一)](/@sysprog/linux2023-fibdrv-a), [(二)](/@sysprog/linux2023-fibdrv-b), [(三)](/@sysprog/linux2023-fibdrv-c), [(四)](/@sysprog/linux2023-fibdrv-d), [(五)](/@sysprog/linux2023-fibdrv-e),記錄你的啟發和疑惑
- [x] 在 GitHub 上 fork [fibdrv](https://github.com/sysprog21/fibdrv),目標是修正 Fibonacci 數計算並改善 `fibdrv` 核心模組的計算效率,過程中需要量化執行時間,可在 Linux 核心模組和使用者空間 (userspace) 中測量
- [x] 原本的程式碼只能列出到 $Fibonacci(100)$ 而且==部分輸出是錯誤的數值==,請修改程式碼,列出後方更多數值 (注意: 檢查正確性和數值系統的使用)
- [x] 務必研讀上述 ==Linux 效能分析的提示== 的描述,降低效能分析過程中的干擾;
- [x] 在 Linux 核心模組中,可用 ktime 系列的 API;
- [x] 在 userspace 可用 [clock_gettime](https://linux.die.net/man/2/clock_gettime) 相關 API;
- [ ] 善用統計模型,除去極端數值,過程中應詳述你的手法
- [x] 分別[用 gnuplot 製圖](https://hackmd.io/@sysprog/Skwp-alOg),分析 Fibonacci 數列在核心計算和傳遞到 userspace 的時間開銷,單位需要用 us 或 ns (自行斟酌)
- [ ] 嘗試解讀上述實驗的時間分佈,特別是隨著 Fibonacci 數列增長後,對於 Linux 核心的影響。可修改 VFS 函式,允許透過寫入 `/dev/fibonacci` 裝置檔案來變更不同的 Fibonacci 求值的實作程式碼。
![](https://i.imgur.com/7xyCUVO.png)
- [ ] 用 printk 固然方便,但其執行時間易受各種因素而不穩定,除了透過統計來觀察,也可改用核心的 sysfs 介面,後者可讓我們自行宣告 show 和 store 介面來提供讀寫操作,可參見 [Sample kobject implementation](https://elixir.bootlin.com/linux/latest/source/samples/kobject/kobject-example.c) (注意: 切換到對應的 Linux 核心版本)。
- [ ] 逐步縮減 Fibonacci 的執行時間,過程中要充分量化
- [ ] 嘗試研讀 [sysprog21/bignum](https://github.com/sysprog21/bignum) 的實作,理解其中的技巧並與你的實作進行效能比較,探討個中值得取鏡之處。
- [ ] 當 $Fib(n)$ 隨著 $n$ 越來越大時,由 Linux 核心傳遞字串形式的十進位就成為效能瓶頸
* 儘量降低由核心傳遞到應用程式的資料量
* 確保應用程式不需要過多的計算量才可重現 Fibonacci 數列
* BONUS: 研讀〈[Integer Encoding Algorithm 筆記](https://kkc.github.io/2021/01/30/integer-compression-note/)〉所提及的策略,在 Linux 核心模組先對數值進行編碼和壓縮,再由應用程式予以顯示 $Fib(n)$ 數值