# Linux 核心專題: 重做 fibdrv > 執行人: [bonianlee](https://github.com/bonianlee/fibdrv) > [專題解說錄影](https://youtu.be/gD8qEwLtwz8) :::success :question: 提問清單 * ? ::: ## 任務簡述 依據 [fibdrv](https://hackmd.io/@sysprog/linux2023-fibdrv) 作業規範,重新進行 (刪去原有的 GitHub repository,做更好的自己!)。 ## TODO: 紀錄閱讀作業說明中所有的疑惑 閱讀 [fibdrv](https://hackmd.io/@sysprog/linux2023-fibdrv) 作業規範,包含「作業說明錄影」和「Code Review 錄影」,本著「誠實面對自己」的心態,在本頁紀錄所有的疑惑,並與授課教師預約討論。 過程中,彙整 [Homework3](https://hackmd.io/@sysprog/linux2023-homework3) 學員的成果,挑選至少三份開發紀錄,提出值得借鏡之處,並重現相關實驗。 ## TODO: 回覆「自我檢查清單」 回答「自我檢查清單」的所有問題,需要附上對應的參考資料和必要的程式碼,以第一手材料 (包含自己設計的實驗) 為佳 ## TODO: 以 [sysprog21/bignum](https://github.com/sysprog21/bignum) 為範本,實作有效的大數運算 理解其中的技巧並導入到 fibdrv 中,並留意以下: * 在 Linux 核心模組中,可用 ktime 系列的 API; * 在 userspace 可用 clock_gettime 相關 API; * 善用統計模型,除去極端數值,過程中應詳述你的手法 * 分別用 gnuplot 製圖,分析 Fibonacci 數列在核心計算和傳遞到 userspace 的時間開銷,單位需要用 us 或 ns (自行斟酌) ## TODO: 儘量降低由核心傳遞到應用程式的資料量 確保應用程式不需要過多的計算量才可重現 Fibonacci 數列,並需要充分驗證 fibdrv 行為。 ## 時間測量與效能分析 分析程式的運行時間,可以作為性能好壞的判斷標準之一。使用 `ktime_t` 中的 `ktime_get()` 來紀錄在 kernel 中運行的時間,並經由 `ktime_sub()` 來計算經歷的時間長,方法如下 ```c static ktime_t kt; ``` 計算 Fibonacci 是在 `fib_read()` 中進行,因此在此函式中計算運行時間 ```c static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { ktime_t start = ktime_get(); long long bn = fib_sequence(*offset); kt = ktime_sub(ktime_get(), start); return (ssize_t) bn; } ``` 參考 [yanjiew1](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) 的時間測量程式,將結果寫入 `TimeTaken.txt` 中,再匯入 Python 繪製數據圖 ![](https://hackmd.io/_uploads/HJw5OZz_2.png) 由圖形可以發現,隨著數列要計算的值越大,運算時間也會花費越多。另外,因為目前還沒有實作大數運算,因此在 Fibonacci sequence 第 92 個數字之後的計算耗時呈現定值 :::info 上面的數據圖是 `fib_sequence()` 尚未修正 Variable-length array (VLA) 在 Linux kernel 中不被允許的問題所繪製的圖,因為我將原本的陣列改為動態陣列後,雖然解決了 VLA 的問題,但是繪製出的圖形較不易看出趨勢,並且因為持續佔用記憶體的關係,導致運算時間較沒有一致性 ![](https://raw.githubusercontent.com/bonianlee/fibdrv/fd9d1ec202ea3e93c194dc47f75bd38203be3a14/scripts/data.png) ::: ### User space 時間測量 可以使用 `time.h` 的 `clock_gettime` 在 user space 中進行時間測量。取得的時間即為 `struct timespec` ,其定義如下 ```c struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; ``` 若要將此結構的時間轉成 `ktime_t` ,只需要將時間單位都換成奈秒 (ns) 再相加即可,因為 `ktime_t` 是奈秒級別的結構 ```c clock_gettime(CLOCK_MONOTONIC, &t); long long time = (long long) (t.tv_sec * 1e9 + t.tv_nsec); ``` ![](https://hackmd.io/_uploads/SygxYWfO2.png) 從繪製出的數據圖發現有幾個極端的數據點,導致數據圖較缺乏參考價值。因此參考 < [用統計手法去除極端值](https://hackmd.io/@sysprog/linux2023-fibdrv-c#%E7%94%A8%E7%B5%B1%E8%A8%88%E6%89%8B%E6%B3%95%E5%8E%BB%E9%99%A4%E6%A5%B5%E7%AB%AF%E5%80%BC) > ,利用實作 [4ce43c4](https://github.com/colinyoyo26/fibdrv/commit/4ce43c4e7ee0b80c4ce9e6dcd995bbfdf239206c) 來將極端值去除,並且將 user space , kernel 以及 kernel to user 的時間花費圖放在一起做比較 ![](https://hackmd.io/_uploads/ry2gtWMdn.png) 理論上, kernel 的運算時間會較少, user space 的運算時間因為有經過資料傳輸,會比 kernel 還要多。從數值圖判斷,測量到的時間趨勢理論上是正確的。 :::info 在參考的程式碼中有一行 code 寫到 ```c Ys.append(np.delete(output, 0, axis=1)) ``` `Ys` 是圖中三個數據所組成的 array ,而上面這行 code 在做的事情是將刪除 $n = 0$ 的數據加入 `Ys` 中。一開始我不解為何要刪掉第一項數據,因此我就將沒有刪除的數據再畫一次圖 ![](https://hackmd.io/_uploads/HkdgohGOn.png) 發現第一項數據會是極端值,因此先做數據刪除的動作。但還是不確定為何不會自動被 `outlier_filter()` 給濾掉 ::: ## 改進費氏數列計算方式 研讀〈[費氏數列](https://hackmd.io/@sysprog/linux2023-fibdrv-a#-%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97)〉,知道費氏數列有多種不同的計算方法,這邊簡述其中三種方式 1. 公式解 $$F(n) = \frac{1}{\sqrt{5}}(\frac{1+\sqrt{5}}{2})^n-\frac{1}{\sqrt{5}}(\frac{1-\sqrt{5}}{2})^n$$ 這種方法的缺點是 $\sqrt{5}$ 無法在計算時得到確切的數值,因此隨著 $n$ 的值增加,次方項的誤差也會跟著增加 2. 遞迴加法 $$F(k) = F(k-1) + F(k-2)$$ 這種算法雖然最基本與直觀,但不是聰明的方式。因為要計算 $F(k)$ 必須將第 $2$ 項到第 $k-1$ 項的數值都計算過一遍,而且每次求新的值都需要重複進行這樣的計算,運算量並沒有進行改善 3. Fast doubling $$ \begin{bmatrix} F(2) \\ F(1) \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} \begin{bmatrix} F(1) \\ F(0) \end{bmatrix} $$ $$\to\ \begin{bmatrix} F(n+1) \\ F(n) \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n} \begin{bmatrix} F(1) \\ F(0) \end{bmatrix} $$ $$\to\ \begin{bmatrix} F(2n+1) \\ F(2n) \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{2n} \begin{bmatrix} F(1) \\ F(0) \end{bmatrix} $$ $$\to\ \begin{bmatrix} F(2n+1) \\ F(2n) \end{bmatrix} = \begin{bmatrix} F(n+1) & F(n) \\ F(n) & F(n-1) \end{bmatrix} \begin{bmatrix} F(n+1) & F(n) \\ F(n) & F(n-1) \end{bmatrix} \begin{bmatrix} 1 \\ 0 \end{bmatrix} $$ $$\to\ \begin{bmatrix} F(2n+1) \\ F(2n) \end{bmatrix} = \begin{bmatrix} F(n+1)^2 + F(n)^2 \\ F(n)F(n+1) + F(n)F(n-1) \end{bmatrix} $$ $$\to\ \begin{bmatrix} F(2n+1) \\ F(2n) \end{bmatrix} = \begin{bmatrix} F(n+1)^2 + F(n)^2 \\ F(n)[2F(n+1) - F(n)] \end{bmatrix} $$ Fast doubling 的概念是只進行必要的運算,不會將目標項以前的每一個數值都計算出來。從上式 $F(2n+1)=F(n+1)^2 + F(n)^2$ 與 $F(2n) = F(n)[2F(n+1) - F(n)]$ 可以知道,想要求出 $F(2n+1)$ 與 $F(2n)$ 只需要用到 $F(n+1)$ 跟 $F(n)$ 項,計算量比遞迴加法少了一半以上 --- ### Fast doubling 實作 參考 [fewletter](https://hackmd.io/@fewletter/linux2023q1-fibdrv) 的實作,將 Fast doubling 的概念以下方程式碼呈現 ```c for (uint64_t i = 1LL << 63; i; i >>= 1) { long long n0 = f[0] * (2 * f[1] - f[0]); long long n1 = f[0] * f[0] + f[1] * f[1]; if (k & i) { f[0] = n1; f[1] = n0 + n1; } else { f[0] = n0; f[1] = n1; } ``` 再次進行時間測量,得到下方數據圖 ![](https://hackmd.io/_uploads/HkueIWG_n.png) 相較於先前的遞迴加法,從數據圖中不難發現計算時間受費氏數列項次的影響盛微,並且 kernel to user 的傳輸時間降低了不少,使得 user space 的耗費時間壓在 1050 奈秒左右,至少比遞迴加法所耗費的時間少了 450 奈秒 ## 支援大數運算 礙於資料型態的空間限制, `long long` 只能儲存 64 位元的資料大小,若費氏數列的值超過 `long long` 所能夠儲存的範圍,則會發生溢位。為了支援大數運算,可以加入以下結構體 ```c typedef struct _bn { unsigned int *number; unsigned int size; } bn; ``` 在結構體中,將大數以 $2^{32}$ 為一個單位做進位, `size` 則儲存單位的數目 而為了要表示資料大小比 `long long` 還要大的數值,必須具備以下條件 - [預先計算儲存 $F(n)$ 所需的空間](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-b#%E9%A0%90%E5%85%88%E8%A8%88%E7%AE%97%E5%84%B2%E5%AD%98-Fn-%E6%89%80%E9%9C%80%E7%9A%84%E7%A9%BA%E9%96%93) 透過公式法先計算出 $F(n)$ 所需使用的位元數 $$ F(n) bit = \frac{-1160964 + 694242*n}{10^6} + 1 $$ ```c int fibn_per_32bit(int k) { return k < 2 ? 1 : ((long) k * 694242 - 1160964) / (1000000) + 1; } ``` - [建立 `bn` 結構體](https://hackmd.io/@sysprog/linux2023-fibdrv-d#%E5%88%9D%E6%AD%A5%E6%94%AF%E6%8F%B4%E5%A4%A7%E6%95%B8%E9%81%8B%E7%AE%97) ```c bn *bn_alloc(size_t size) { bn *new = kmalloc(sizeof(bn), GFP_KERNEL); new->number = kmalloc(sizeof(int) * size, GFP_KERNEL); memset(new->number, 0, sizeof(int) * size); new->size = size; return new; } ``` - 清除 `bn` ```c int bn_free(bn *src) { if (src == NULL) return -1; kfree(src->number); kfree(src); return 0; } ``` --- ### [`bn` 運算](https://hackmd.io/@sysprog/linux2023-fibdrv-d#%E5%88%9D%E6%AD%A5%E6%94%AF%E6%8F%B4%E5%A4%A7%E6%95%B8%E9%81%8B%E7%AE%97) - `bn_add` $2^{32}$ 進位的加法,先算低位元的加法,用 unsigned long long int 去儲存結果,若有進位的數值,則會在右移 32 位元之後出現 ```c void bn_add(bn *a, bn *b, bn *c) { unsigned long long int carry = 0; for (int i = 0; i < c->size; i++) { unsigned int tmp1 = (i < a->size) ? a->number[i] : 0; unsigned int tmp2 = (i < b->size) ? b->number[i] : 0; carry += (unsigned long long int) tmp1 + tmp2; c->number[i] = carry; carry >>= 32; } } ``` - `bn_swap` 互換兩個 `bn` 的位置,常用於迭代 ```c void bn_swap(bn *a, bn *b) { bn tmp = *a; *a = *b; *b = tmp; } ``` - `bn_to_string` 將數字和 `0` 的差以 char 陣列儲存,並且從高位逐一檢查是否有進位,若有進位則處理成沒有進位的數字 ```c char *bn_to_string(bn src) { size_t len = (8 * sizeof(int) * src.size) / 3 + 2; char *s = kmalloc(len, GFP_KERNEL); char *p = s; memset(s, '0', len - 1); s[len - 1] = '\0'; for (int i = src.size - 1; i >= 0; i--) { for (unsigned int d = 1U << 31; d; d >>= 1) { /* binary -> decimal string */ int carry = !!(d & src.number[i]); for (int j = len - 2; j >= 0; j--) { s[j] += s[j] - '0' + carry; // double it carry = (s[j] > '9'); if (carry) s[j] -= 10; } } } // skip leading zero while (p[0] == '0' && p[1] != '\0') { p++; } memmove(s, p, strlen(p) + 1); return s; } ``` --- ### 將 `bn` 加入費氏數列的運算 參考[程式碼](https://hackmd.io/@sysprog/linux2023-fibdrv-d#%E6%AD%A3%E7%A2%BA%E8%A8%88%E7%AE%97-F92-%E4%BB%A5%E5%BE%8C%E7%9A%84%E6%95%B8%E5%80%BC)將 `bn` 加入費氏數列的計算 ```c void bn_fib(bn *ret, unsigned int n) { int nsize = fibn_per_32bit(n) / 32 + 1; bn_resize(ret, nsize); if (n < 2) { ret->number[0] = n; return; } bn *f1 = bn_alloc(nsize); bn *tmp = bn_alloc(nsize); ret->number[0] = 0; f1->number[0] = 1; for (unsigned int i = 1; i < n + 1; i++) { bn_add(ret, f1, tmp); bn_swap(f1, ret); bn_swap(f1, tmp); } bn_free(f1); bn_free(tmp); } ``` 接著將加入大數運算的程式進行時間測量,把 $n$ 設為 1000 觀察數據圖的趨勢 ![](https://hackmd.io/_uploads/ByeERJz4O3.png) 從結果來看,此圖形是合理的。隨著計算量變大,運算時間也跟著拉長,同時因為資料傳輸量變大的緣故, user space 所耗費的時間相比於在 kernel 進行運算要長很多