# Linux 核心專題: 重做 fibdrv > 執行人: paulpeng-popo > [專題解說錄影](https://youtu.be/yPyiDiKKO6k) > [GitHub](https://github.com/paulpeng-popo/fibdrv) :::success :question: 提問清單 * ? ::: ## 任務簡述 依據 [fibdrv](https://hackmd.io/@sysprog/linux2023-fibdrv) 作業規範,繼續投入 Linux 核心模組和相關程式的開發。 ### 公式推導 假設成年兔子為 `a`,幼年兔子為 `b`,且兔子不死亡 $a_{n+1} = a_n + b_n$ $b_{n+1} = a_n$ ==一般遞迴== $a_{n+1} = a_{n-1} + a_n, n \ge 1$ $a_0 = 0, a_1 = 1$ ==矩陣運算 Q-matrix== $\begin{bmatrix} a_{n+1} \\ b_{n+1} \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} \begin{bmatrix} a_n \\ b_n \end{bmatrix}$ 又可改寫成 $\begin{bmatrix} a_{n+1} \\ a_n \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} \begin{bmatrix} a_n \\ a_{n-1} \end{bmatrix}$ $\begin{bmatrix} a_{n+1} & a_n \\ a_n & a_{n-1} \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^n$ ==Fast Doubling== $F(0) = a_0 = 0$ $F(1) = a_1 = 1$ $$ \begin{split} \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}\\ \\ &= \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^n \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^n \begin{bmatrix} F(1) \\ F(0) \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}\\ \\ &= \begin{bmatrix} F(n+1)^2 + F(n)^2\\ F(n)F(n+1) + F(n-1)F(n) \end{bmatrix} \end{split} $$ 其中 $F(n-1) = F(n+1) - F(n)$ 最後得到 $F(2k) = F(k)[ 2F(k+1)-F(k) ]$ $F(2k+1) = F(k+1)^2 + F(k)^2$ ### Fast Doubling $F(2k) = F(k)[ 2F(k+1)-F(k) ]$ $F(2k+1) = F(k+1)^2 + F(k)^2$ 作業說明中的虛擬碼 ```clike Fast_Fib(n) a = 0; b = 1; // m = 0 for i = (number of binary digit in n) to 1 t1 = a*(2*b - a); t2 = b^2 + a^2; a = t1; b = t2; // m *= 2 if (current binary digit == 1) t1 = a + b; // m++ a = b; b = t1; return a; ``` ### 原理 這裡 Fast Doubling 利用 **比 $n$ 兩倍小的 $n'$** 來算出 $F(n)$,遞迴的深度則被限制在 $O(\log{n})$ 內,這可以大幅減少重複的計算 參考:[Calculating Fibonacci Numbers by Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling) ```c /* FILE: fibdrv.c */ static long long fib_fastd(long long n) { if (n < 2) return n; long long a = 0; long long b = 1; for (int j = 63 - 1; j >= 0; j--) { long long c = a * ((b << 1) - a); long long d = a * a + b * b; a = c; b = d; if ((n >> j) & 1LL) { a = d; b = c + d; } } return a; } ``` n 小於 2,直接回傳,不會經過迴圈 從 n 的 MSB 開始,依序計算該數的費氏數,直到 n 的 LSB 或剩下之 bit 皆為 0,則提前跳出迴圈 先用 `h` 記錄實際的數字共有幾個 bit (不含 leading 0s),接著從 MSB 開始計算首項費氏數,再藉由迴圈依次增加 `n >> j` 的 bit 數,以計算後續的費氏數 **複雜度分析** - 每次迭代使用 3 個乘法跟 2 個加法,時間複雜度為 $O(1)$ - 根據輸入的 n 大小決定迭代次數,即最多做 $O(log n)$ 次遞迴 - 總共時間複雜度為 $O(1) * O(log n) = O(log n)$ 解釋 fast doubling ==為什麼用 2k 跟 2k + 1== 例如:**長度為 n 個 bit 的數字 $\alpha$** 與 **長度為 n+1 個 bit 的數字 $\beta$** 具有以下關係: - if $\beta$ 要為偶數,則 $\beta = 2\alpha$,也就是 $\beta$ = $\alpha$ << 1 - if $\beta$ 要為奇數,則 $\beta = 2\alpha + 1$,也就是 $\beta$ = $\alpha$ << 1 + 1 剛好就是 binary number 的進位規則 使用 `__builtin_clzll` function 做硬體上的計算加速 `clz` (Count Leading Zero) 可以計算數字 `n` 前面有幾個 0,加速取得 `h` 的速度 ```diff - for (int j = 63 - 1; j >= 0; j--) { + long long mask = 1LL << (63 - __builtin_clzll(n)); + for (; mask; mask >>= 1) { - if ((n >> j) & 1LL) { + if (mask & n) { ``` ### 實作大數運算前的效能比較 #### VLA 改進 Linux kernel 不支援 VLA,因此使用 Dynamic Programming 與空間重複利用的技巧,避免動態陣列的使用,同時將 space complexity 從 $O(n)$ 降到 $O(1)$ > Linux 核心限制使用 VLA 的因素 > 在 [The Linux Kernel is now VLA-Free](https://www.phoronix.com/news/Linux-Kills-The-VLA) 有提到 Linux kernel 不再允許 VLA 的使用 > > C99 具有 VLA (Variable-Length Array) 的特性,允許執行時期再決定陣列佔用的空間大小,但可能會造成 stack overflow 等安全疑慮 根據教材 [你所不知道的 C 語言:函式呼叫篇](https://hackmd.io/@sysprog/c-function#stack-based-buffer-overflow),攻擊者可以利用 stack-based-buffer-overflow 來執行惡意程式碼,從而對系統造成威脅 > > 此外,Linus Torvalds 也說過:[USING VLA'S IS ACTIVELY STUPID! It generates much more code, and much _slower_ code (and more fragile code), than just using a fixed key size would have done.](https://lkml.org/lkml/2018/3/7/621)』 ```c /* FILE: fibdrv.c */ static long long fib_sequence(long long k) { long long f[2] = {0, 1}; if (k < 2) return f[k]; for (int i = 2; i <= k; i++) { f[i & 1] = f[0] + f[1]; } return f[k & 1]; } ``` #### 時間測量功能 > 參考 [時間測量與效能分析](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-c) 的說明與 [qwe661234](https://hackmd.io/@qwe661234/linux2022q1-homework3#%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90) 的解釋 > 1. [High resolution timers and dynamic ticks design notes](https://docs.kernel.org/timers/highres.html) > 2. [hrtimers - subsystem for high-resolution kernel timers](https://docs.kernel.org/timers/hrtimers.html) 在 Linux 核心中具有兩種計時方式 ==jiffies== 是 Linux kernel 較早期存在的計時方法,使用作業系統**從開機以來的計時器中斷發生的次數**作為計時的依據,此全域變數稱作 `ticks` 數。計時器中斷為週期性的中斷事件,以固定的時間間隔觸發,利用間隔固定的特性便能達到測量時間的目的。 通常情況下,jiffies 的精度為毫秒級別,具體的精度取決於 tick rate 的大小。例如 1000Hz 表示每秒觸發 1000 次的 timer interrupt,因此 jiffies 的增加速度為每毫秒一次。 jiffies 轉換為**秒**的公式為 ```c (unsigned long long)(jiffies - INITIAL_JIFFIES) / HZ; ``` 由以上程式碼可知,假如系統原先設定的 `tick rate (HZ)` 不大於 $10^9$,其時間精度是無法達到 ns 等級的。 ==hrtimer== 是在 2.6.16 開始有的新的計時機制,裡面使用了 `ktime_t` 這個資料結構來進行計時。書中提到,原本基於 `jiffies` 的計時機制 `timer wheel` 在許多層面上有許多缺失,並幾乎無法再優化/整合了,包括: > 1. The forced handling of low-resolution and high-resolution timers in the same way leads to a lot of compromises, macro magic and #ifdef mess. > 2. The unpredictable [O(N)] overhead of cascading leads to delays which necessitate a more complex handling of high resolution timers, which in turn decreases robustness. > 3. The timer wheel data structure is too rigid for high-res timers. 因此實作 `hrtimer` 的設計考量包括 - simplicity - data structure not bound to jiffies or any other granularity. All the kernel logic works at 64-bit nanoseconds resolution - no compromises - simplification of existing, timing related kernel code ktime_t 結構體定義如下 ```c union ktime { s64 tv64; }; typedef union ktime ktime_t; ``` 其結構體內有一型態為 64 位元的有號整數 tv64 跟據 Linux 原始碼 [include/linux/ktime.h](https://github.com/torvalds/linux/blob/master/include/linux/ktime.h), `ktime_t` 的單位為奈秒 (ns) 透過呼叫 [Linux kernel API](https://elixir.bootlin.com/linux/v4.4/source/kernel/time/timekeeping.c#L665) `ktime_get()`,可以取得**相對於系統啟動時間的偏移量**,因此藉由計算出兩次 `ktime_get()` 的時間差異,便可知其之間的程式碼執行時間花費 ```c /* FILE: fibdrv.c */ static ktime_t kt; static long long fib_time_proxy(long long k) { kt = ktime_get(); long long result = fib_sequence(k); kt = ktime_sub(ktime_get(), kt); return result; } static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { return (ssize_t) fib_time_proxy(*offset); } static ssize_t fib_write(struct file *file, const char *buf, size_t size, loff_t *offset) { return ktime_to_ns(kt); } ``` 在 read 的時候,計算 fib,並同時將所花時間儲存在 `kt` 中 接著 write 的時候,將 `kt` 的值轉換成 nanoseconds 再回傳給 client `client` 中順序呼叫 `read` 和 `write`,即可取得所花費的 kernel time ```c /* FILE: client.c */ for (int i = 0; i <= offset; i++) { lseek(fd, i, SEEK_SET); sz = read(fd, buf, 1); kt = write(fd, write_buf, 1); } ``` 執行 `sudo ./client` 後,成功輸出計算費氏數時間,輸出如下 ``` Reading from /dev/fibonacci at offset 0, returned the sequence 0. Time: 331 (ns). Reading from /dev/fibonacci at offset 1, returned the sequence 1. Time: 197 (ns). Reading from /dev/fibonacci at offset 2, returned the sequence 1. Time: 183 (ns). Reading from /dev/fibonacci at offset 3, returned the sequence 2. Time: 113 (ns). Reading from /dev/fibonacci at offset 4, returned the sequence 3. Time: 123 (ns). Reading from /dev/fibonacci at offset 5, returned the sequence 5. Time: 120 (ns). Reading from /dev/fibonacci at offset 6, returned the sequence 8. Time: 81 (ns). Reading from /dev/fibonacci at offset 7, returned the sequence 13. Time: 78 (ns). Reading from /dev/fibonacci at offset 8, returned the sequence 21. ... ``` #### 選擇演算法 > 參考 [yanjiew1](https://hackmd.io/@yanjiew/linux2023q1-fibdrv) 的做法,利用 `fib_read` 傳入 `size` 作為使用不同演算法的條件 ```c static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) ``` ```c /* FILE: fibdrv.c */ switch (size) { case 1: result = fib_sequence(k); break; case 2: result = fib_fastd(k); break; case 3: result = fib_fastd_clz(k); break; default: return -EINVAL; } ``` 使用 gnuplot 製圖,確認 fast doubling 確實加速費氏數的計算,同時發現 `__builtin_clzll` 大幅降低時間開銷 ![](https://hackmd.io/_uploads/HkAnHsWuh.png) NOTE: 此處時間為 kernel mode 中計算的時間,還未加入 user space 的時間開銷比較 #### 排除效能干擾因素 使用 `taskset` 預留 cpu 給效能測試使用 1. `$ sudo vim /etc/default/grub` 2. 修改此行 `GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=5"` 3. `$ sudo update-grub` 4. `$ sudo init 6` 將作業說明提到的方法寫成一個 script,包括: 1. 關閉 ASLR(address space layout randomization 2. 將 cpufrq 從 powersave 變成 performance 3. 關閉 turbo mode 4. 取消 cpu smp affinity ```shell #!/bin/sh CPUID=5 MASK=$((1 << $CPUID)) MASK=$((0xfff & ~$MASK)) MASK=`printf "%x" $MASK` ORIG_ASLR=`cat /proc/sys/kernel/randomize_va_space` ORIG_GOV=`cat /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor` ORIG_TURBO=`cat /sys/devices/system/cpu/intel_pstate/no_turbo` ORIG_IRQ=`cat /proc/irq/$CPUID/smp_affinity` sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo" sudo sh -c "echo performance > /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor" for file in /proc/irq/*/smp_affinity; do var=0x$(cat "$file") var=$((var & 0xfdf)) var=`printf "%x" $var` sudo sh -c "echo $var > $file" 2> /dev/null done sudo sh -c "echo $MASK > /proc/irq/$CPUID/smp_affinity" # Load the module and run the client make unload make load python3 scripts/statisic_plot.py -cpu=$CPUID gnuplot -e "filename='scripts/data.txt'" scripts/draw.gp make unload # Restore original settings sudo sh -c "echo $ORIG_ASLR > /proc/sys/kernel/randomize_va_space" sudo sh -c "echo $ORIG_GOV > /sys/devices/system/cpu/cpu$CPUID/cpufreq/scaling_governor" sudo sh -c "echo $ORIG_TURBO > /sys/devices/system/cpu/intel_pstate/no_turbo" sudo sh -c "echo $ORIG_IRQ > /proc/irq/$CPUID/smp_affinity" ``` 最後使用 `taskset -c 5 ./client` 指定 client 使用 cpu 5 進行費氏數計算 #### 去除極端值 參考 [時間測量與效能分析](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-c) 計算 50 次費氏數的平均,並將離散值做處理,用以得到較平滑的效能比較圖 去除的 outlier 以超過==兩個標準==差為條件,移除離 50 次平均值太遠的數據 > [commit 4b6a40c](https://github.com/paulpeng-popo/fibdrv/commit/4b6a40cd4785434a5250e6ef80409fb5e2ea2561#diff-310bc6068100c1b144bacada83ff9be0125cea94160c62aea2ca0b15c3e566fa) ![](https://hackmd.io/_uploads/HkcaqzEO3.png) 以常態分佈曲線來看,當我們排除==兩個標準==以外的數據後,代表此統計手法是在 95% 之信賴區間內取樣平均 ```python def outlier_filter(datas, threshold = 2): datas = np.array(datas) z = np.abs((datas - datas.mean()) / datas.std()) return datas[z < threshold] def data_processing(data_set, n): catgories = data_set[0].shape[0] samples = data_set[0].shape[1] final = np.zeros((catgories, samples)) for c in range(catgories): for s in range(1, samples): final[c][0] = data_set[0][c][0] final[c][s] = \ outlier_filter([data_set[i][c][s] for i in range(n)]).mean() return final ``` ## TODO: 紀錄閱讀作業說明中所有的疑惑 閱讀 [fibdrv](https://hackmd.io/@sysprog/linux2023-fibdrv) 作業規範,包含「作業說明錄影」和「Code Review 錄影」,本著「誠實面對自己」的心態,在本頁紀錄所有的疑惑,並與授課教師預約討論。 過程中,彙整 [Homework3](https://hackmd.io/@sysprog/linux2023-homework3) 學員的成果,挑選至少三份開發紀錄,提出值得借鏡之處,並重現相關實驗。 > Todo: 嘗試使用 `mlock` 改善 page fault 的情況 > [2020 - KYG-yaya573142](https://hackmd.io/@KYWeng/rkGdultSU#system-call-overhead) > Todo: 嘗試使用 `sysfs` > [willwillhi1](https://hackmd.io/@willwillhi/linux2023q1-fibdrv#%E5%A4%A7%E6%95%B8%E9%81%8B%E7%AE%97) ## TODO: 回覆「自我檢查清單」 回答「自我檢查清單」的所有問題,需要附上對應的參考資料和必要的程式碼,以第一手材料 (包含自己設計的實驗) 為佳 - [x] 研讀上述 ==Linux 效能分析的提示== 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 $\to$ 從中也該理解為何不希望在虛擬機器中進行實驗 - 虛擬機技術通常使用虛擬化層來模擬硬體資源,這個虛擬化層介於實際硬體和作業系統之間,可能會引入效能上的延遲或不準確的測量結果。 - 虛擬環境中可能有多個虛擬機共享同一個實際硬體資源,意味著資源分配可能會導致效能變異性。 - 虛擬機中的時間處理不同於實際硬體。虛擬機軟體必須模擬時間的流逝,並為虛擬機中的作業系統和應用程式提供時間服務。 - [ ] 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 [clz / ctz](https://en.wikipedia.org/wiki/Find_first_set) 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說 - [ ] 複習 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)〉 ### 觀察 Linux 核心模組中的 mutex fibdrv 中的 mutex 使用在 `open` 和 `release` 的部份,也就是一次只允許一個 thread 能夠存取 character device ```c static int fib_open(struct inode *inode, struct file *file) { if (!mutex_trylock(&fib_mutex)) { printk(KERN_ALERT "fibdrv is in use"); return -EBUSY; } return 0; } static int fib_release(struct inode *inode, struct file *file) { mutex_unlock(&fib_mutex); return 0; } ``` 現在嘗試將 mutex lock 移除 ```c static int fib_open(struct inode *inode, struct file *file) { /* if (!mutex_trylock(&fib_mutex)) { printk(KERN_ALERT "fibdrv is in use"); return -EBUSY; } */ return 0; } static int fib_release(struct inode *inode, struct file *file) { //mutex_unlock(&fib_mutex); return 0; } ``` 撰寫簡單的 POSIX thread 程式來測試 例如:`thread-1` 和 `thread-2` 都同時打開 fibdrc device,讀取 fib(0) ~ fib(49),並且為了使用延遲讓兩個 thread 交替執行,營造 race condition 的情況 ```c static int fd; void *job(void* arg){ char buf[100]; int id = *((int *) arg); for (int i = 0; i <= 50; i++) { lseek(fd, i, SEEK_SET); sleep(id); read(fd, buf, 0); printf("thread %d read fib(%d): %s\n", id, i, buf); } } int main(){ fd = open("/dev/fibonacci", O_RDWR); if (fd < 0) { perror("Failed to open character device"); exit(1); } pthread_t test[2]; int pid1 = 1; pthread_create(&test[0], NULL, job, (void *), &pid1); int pid2 = 2; pthread_create(&test[1], NULL, job, (void *), &pid2); for(int i = 0; i < 2; i++) pthread_join(test[i], NULL); close(fd); return 0; } ``` 這邊可以看到前幾項發生錯誤了,thread-2 讀取 fib(2) 的值為 2,說明在 thread-2 設定好的 offest 並將要透過 read() 進入 kernel space 讀取之前,file offset 被 thread-1 更動過了,造成錯誤結果 ``` thread 1 read fib(0): 0 thread 2 read fib(0): 1 thread 1 read fib(1): 1 thread 1 read fib(2): 1 thread 2 read fib(1): 2 <------- thread 1 read fib(3): 1 thread 1 read fib(4): 3 thread 2 read fib(2): 5 thread 1 read fib(5): 2 thread 1 read fib(6): 8 ``` 因此我們需要在 device 被 open() 時將 mutex 上鎖,而 release() 時將 mutex 解鎖,保持同時只有一個 thread 能夠控制 device driver ## TODO: 以 [sysprog21/bignum](https://github.com/sysprog21/bignum) 為範本,實作有效的大數運算 理解其中的技巧並導入到 fibdrv 中,並留意以下: * 在 Linux 核心模組中,可用 ktime 系列的 API; * 在 userspace 可用 clock_gettime 相關 API; * 善用統計模型,除去極端數值,過程中應詳述你的手法 * 分別用 gnuplot 製圖,分析 Fibonacci 數列在核心計算和傳遞到 userspace 的時間開銷,單位需要用 us 或 ns (自行斟酌) ### 嘗試引入 [sysprog21/bignum](https://github.com/sysprog21/bignum) 程式碼,使 fib 核心模組可以計算 93 以後的數值 > [commit 4105efe](https://github.com/paulpeng-popo/fibdrv/commit/4105efeba818c6a222236a8ba6d62424149befe1) > Todo: 分析細部的程式實作方法,並思考能否加速 > 參考 [bakudr18](https://hackmd.io/@bakudr18/HJ363p_Qd#%E5%A4%A7%E6%95%B8%E9%81%8B%E7%AE%97) ```c typedef struct { uint64_t *digits; /* Digits of number. */ uint32_t size; /* Length of number. */ uint32_t alloc; /* Size of allocation. */ unsigned sign : 1; /* Sign bit. */ } ``` - CASE 1: - $digits[0] = 2$ - $digits[1] = 1$ - bignum = $1 * 2^{64^1} + 2 * 2^{64^0}$ - CASE 2: - $digits[0] = 3$ - $digits[1] = 2$ - $digits[2] = 1$ - bignum = $1 * 2^{64^2} + 2 * 2^{64^1} + 3 * 2^{64^0}$ ### 大數運算之 fast doubling ```c bn *a1 = fib; /* Use output param fib as a1 */ bn_t a0, tmp, a; bn_init_u32(a0, 0); /* a0 = 0 */ bn_set_u32(a1, 1); /* a1 = 1 */ bn_init(tmp); /* tmp = 0 */ bn_init(a); /* Start at second-highest bit set. */ for (uint64_t k = ((uint64_t) 1) << (62 - __builtin_clzll(n)); k; k >>= 1) { /* Both ways use two squares, two adds, one multipy and one shift. */ bn_lshift(a0, 1, a); /* a03 = a0 * 2 */ bn_add(a, a1, a); /* ... + a1 */ bn_sqr(a1, tmp); /* tmp = a1^2 */ bn_sqr(a0, a0); /* a0 = a0 * a0 */ bn_add(a0, tmp, a0); /* ... + a1 * a1 */ bn_mul(a1, a, a1); /* a1 = a1 * a */ if (k & n) { bn_swap(a1, a0); /* a1 <-> a0 */ bn_add(a0, a1, a1); /* a1 += a0 */ } } /* Now a1 (alias of output parameter fib) = F[n] */ ``` 修改 `verify.py` 使 python 腳本可透過參數更動測試的項數大小 ```python # FILE: verify.py parser = argparse.ArgumentParser() parser.add_argument('-k', '--max_k', type=int, default=500) args = parser.parse_args() verify_fib(args.max_k) ``` 修改 `client.c` ```c int offset = MAX_FIB_K; ``` 在編譯 client 時添加 `-D` flag 用作條件編譯 ```makefile client: client.c $(CC) -o $@ $^ -DMAX_FIB_K=$(FIB_K) ``` 執行 ```sh $ make check FIB_K=5000 ``` 得到輸出 ``` make load make[1]: Entering directory '/home/pengpaul/linux2023/fibdrv' sudo insmod fibdrv_new.ko make[1]: Leaving directory '/home/pengpaul/linux2023/fibdrv' sudo ./client > out make unload make[1]: Entering directory '/home/pengpaul/linux2023/fibdrv' sudo rmmod fibdrv_new || true >/dev/null make[1]: Leaving directory '/home/pengpaul/linux2023/fibdrv' Passed [-] ``` 確認在 $fib(5000)$ 以前其演算法的正確性 ### 實作大數運算後的效能比較 #### 使用者模式時間同步 因為要測量運算完成後,把資料從核心搬到使用者模式,及使用者模式收到資料的時間。故在使用者模式取得到時間要能跟核心模式進行對應。 在使用者模式可以用 `clock_gettime` 函式來取得時間,其中又有不同的 clock id 。跟據 Linux 核心 [ktime API 文件](https://www.kernel.org/doc/html/latest/core-api/timekeeping.html),在 Linux 核心內,以 `ktime_get` 函式所取得的時間可以對應到 `CLOCK_MONOTONIC` 的時間。 > [探討 clock_gettime 造成 page fault 的原因](https://hackmd.io/@bakudr18/HJ363p_Qd#%E6%8E%A2%E8%A8%8E-clock_gettime-%E9%80%A0%E6%88%90-page-fault-%E7%9A%84%E5%8E%9F%E5%9B%A0) ```c struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; ``` 從 `clock_gettime` 取得的時間結構定義如上,我們可以簡單的將 `tv_sec` * $10^9$ 來換算成與 ktime 對應的 nanoseconds ```c struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, &start); long long sz = read(fd, buf, sizeof(buf)); clock_gettime(CLOCK_MONOTONIC, &end); long long ut = (long long) (end.tv_sec - start.tv_sec) * \ 1e9 + (end.tv_nsec - start.tv_nsec); ``` 通過 user time - kernel time 可以得出 `copy_to_user` system call 的花費時間 依據 [Hierarchical performance measurement and modeling of the Linux file system](https://www.researchgate.net/publication/221556402_Hierarchical_performance_measurement_and_modeling_of_the_Linux_file_system) 研究指出,從核心模式複製資料到使用者層級的時間成本是,每個位元組達 22ns。也就是當數字越大,需傳遞的字串越長,時間花費就越多 如圖所示,隨著 n 增加,system call 開銷明顯可見地大幅增加 ![](https://hackmd.io/_uploads/HJoV_GVun.png) 當把 n 設到 500 時程式執行的效率越來越差 ![](https://hackmd.io/_uploads/rJdyjJNuh.png) > 有產生階梯圖的狀況,每次階梯的產生剛好發生在 $fib(n)$ 的數值超過一個 `uint64_t` 可表示的大小時,與 [paul90317](https://hackmd.io/@paul90317/2023q1-fibdrv#Bottom-up) 同學遇到相似的結果,但原因可能有待釐清 在[作業三說明](https://hackmd.io/@sysprog/linux2023-fibdrv/https%3A%2F%2Fhackmd.io%2F%40sysprog%2Flinux2023-fibdrv-g)中,老師有給出三種提升效能的方法 1. 儘量降低由核心傳遞到應用程式的資料量 2. 確保應用程式不需要過多的計算量才可重現 Fibonacci 數列 3. Integer Encoding Algorithm 技巧 針對第一種降低資料傳遞量的做法,可以將 `bn` 結構體先回傳到 user space,再去做 `bn_to_str` 的操作 ```c char *bn_to_str(const unsigned long long *digits, size_t size) { size_t str_size = (size_t) (size * DIGIT_BITS) / 3 + 1; if (size == 0) { char *str = malloc(2); str[0] = '0'; str[1] = '\0'; return str; } else { char *s = malloc(str_size); char *p = s; memset(s, '0', str_size - 1); s[str_size - 1] = '\0'; /* n.digits[0] contains least significant bits */ for (int i = size - 1; i >= 0; i--) { /* walk through every bit of bn */ for (unsigned long long d = 1ULL << 63; d; d >>= 1) { /* binary -> decimal string */ int carry = !!(d & digits[i]); for (int j = str_size - 2; j >= 0; j--) { s[j] += s[j] - '0' + carry; 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; } } ``` > Todo: 減少 `bn_to_str()` 迴圈和分支的使用情況 實驗證明,`copy_to_user` 複製 `bn` 結構體比複製字串還來的節省時間 system call overhead 維持在 1000 ns 左右,比複製字串的 2000 ns 好上許多 ![](https://hackmd.io/_uploads/rJGnOGNun.png) ![](https://hackmd.io/_uploads/BJzZZgVd2.png) ## TODO: 實作更快速的乘法運算 參照 [Schönhage–Strassen algorithm](https://en.wikipedia.org/wiki/Sch%C3%B6nhage%E2%80%93Strassen_algorithm),在上述大數運算的程式碼基礎之上,改進乘法運算,確保在大多數的案例均可加速,需要有對應的驗證機制。