# 2022q1 Homework3 (fibdrv) contributed by < `vacantron` > ## 自我檢查清單 - [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 數快速計算演算法的實作中如何減少乘法運算的成本; - [ ] 研讀 [KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU),指出針對大數運算,有哪些加速運算和縮減記憶體操作成本的舉措? - [ ] `lsmod` 的輸出結果有一欄名為 `Used by`,這是 "each module's use count and a list of referring modules",但如何實作出來呢?模組間的相依性和實際使用次數 ([reference counting](https://en.wikipedia.org/wiki/Reference_counting)) 在 Linux 核心如何追蹤呢? > 搭配閱讀 [The Linux driver implementer’s API guide » Driver Basics](https://www.kernel.org/doc/html/latest/driver-api/basics.html) - [ ] 注意到 `fibdrv.c` 存在著 `DEFINE_MUTEX`, `mutex_trylock`, `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 [POSIX Thread](https://en.wikipedia.org/wiki/POSIX_Threads) 的程式碼來確認。 ---- ### Fast doubling: Fibonacci 數列經過整理後: $$ F_{2n} = F_n (2 F_{n+1} - F_n) $$ $$ F_{2n+1} = F_{n+1} ^ 2 + F_n ^ 2 $$ 可以由索引值較小的數求出特定的索引值的數。在計算的過程中可能會產生不需要的值,如:由 $F_5, F_6$ 計算 $F_11$ 時 #### top-down 將索引值 k 除以 2 並無條件捨去,算出可以要由哪項 n 得到該索引的值,如 $F_12$ 可以由 $F_6$ 與 $F_7$ 組成。若將兩個數表示成 [$F_k$, $F_{k+1}$],若 k 是偶數,則可以再向下分解為 [$F_n$, $F_{n+1}$] (k = 2n);若 k 是奇數,需要多一個步驟將 [$F_n$, $F_{n+1}$] 得出的 [$F_2n$, $F_{2n+1}$] 相加才能得到 $F_{2n+2}$ 這一項。 #### bottom-up 可以利用 stack 先算出需要的 n,由大到小推入堆疊中後迭代地取出,並根據 n 的奇偶性分別代入最前面的公式得出結果。 若將索引值以二進位表示,由左向右讀取的話就與 top-down 方式相似。若尋訪到的位元為 1,與上述 k 為奇數的處理方法相同,如下所示: ```c // reference: https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling uint64_t fib(unsigned int n) { // loop log2(n) times unsigned int h = 0; for (unsigned int i = n ; i ; ++h, i >>= 1); uint64_t a = 0; // F(0) = 0 uint64_t b = 1; // F(1) = 1 for (unsigned int mask = 1 << (h - 1) ; mask ; mask >>= 1) { uint64_t c = a * (2 * b - a); // F(2k) = F(k) * [ 2 * F(k+1) – F(k) ] uint64_t d = a * a + b * b; // F(2k+1) = F(k)^2 + F(k+1)^2 if (mask & n) { a = d; // F(n_j) = F(2k + 1) b = c + d; // F(n_j + 1) = F(2k + 2) = F(2k) + F(2k + 1) } else { a = c; // F(n_j) = F(2k) b = d; // F(n_j + 1) = F(2k + 1) } } return a; } ``` 使用 `__builtin_clz()` 取代用位移計算 log2(n) ```c uint64_t fib_clz(unsigned int n) { uint64_t a = 0; // F(0) = 0 uint64_t b = 1; // F(1) = 1 for (unsigned int mask = 1 << (sizeof(n) * 8 - __builtin_clz(n)); mask ; mask >>= 1) { uint64_t c = a * (2 * b - a); // F(2k) = F(k) * [ 2 * F(k+1) – F(k) ] uint64_t d = a * a + b * b; // F(2k+1) = F(k)^2 + F(k+1)^2 if (mask & n) { a = d; // F(n_j) = F(2k + 1) b = c + d; // F(n_j + 1) = F(2k + 2) = F(2k) + F(2k + 1) } else { a = c; // F(n_j) = F(2k) b = d; // F(n_j + 1) = F(2k + 1) } } return a; } ``` 使用陣列儲存,以陣列索引為 Fibonacci 數列的索引 ```c uint64_t fib_array(unsigned int n) { uint64_t arr[n]; arr[0] = 0; arr[1] = 1; for(int i = 2; i < n + 1; i++) { arr[i] = arr[i - 1] + arr[i - 2]; } return arr[n]; } ``` 原始迭代版本 ```c uint64_t fib_literal(unsigned int n) { if (n == 0) return 0; if (n == 1) return 1; uint64_t a = 0; // F(0) = 0 uint64_t b = 1; // F(1) = 1 for(int i = 2; i < n + 1; i++) { uint64_t t = a; a = b; b += t; } return b; } ``` 效能對比: ------ ### 核心模組 使用自定義的結構體來儲存溢位的整數,並在讀取檔案時使用 `copy_to_user` 將結構體的內容回傳至 userspace 。在 client 也要對自定義的結構體處理才能如期輸出。 ```c struct u128_t { uint64_t upper; uint64_t lower; }; unsigned __int128 res = fib_sequence(*offset); struct u128_t u128; u128.upper = (uint64_t) res / pow2_64s; u128.lower = (uint64_t) res % pow2_64s; if (copy_to_user(buf, &u128, size)) return -EFAULT; void print_u128(int i, struct u128_t u128) { if (u128.upper != 0) { printf("Reading from " FIB_DEV " at offset %d, returned the sequence " "%llu%llu.\n", i, u128.upper, u128.lower); } else { printf("Reading from " FIB_DEV " at offset %d, returned the sequence " "%llu.\n", i, u128.lower); } } ``` 這裡實作時發現轉送回去的數值有部份有錯誤,在 gdb 中發現在溢位時 ($F_{94}$ 開始) upper 反而變成 0,高位元的資料不見導致後來相加的數值有誤。 []() 一開始是直接使用 `__uint128_t` 型別的指標透過 `copy_to_user` 轉送給 userspace,但是在資料型別的轉換上似乎沒有處理好,導致在對指標取值的時候一直拿不到正確的數值;但測試後發現在 kernel space 確實有算出正確的值,但在轉送的過程中遺失了,在 userspace 只能拿到一個指向一個連續記憶體空間的指標,後來由自定義資料結構解決問題。 gcc 的 [文件](https://gcc.gnu.org/onlinedocs/gcc/_005f_005fint128.html) 中也有提到 `__int128_t` 的資料型態無法直接指派或轉型給小於 128 位元的整數。 中途在使用陣列儲存已計算的數值時不小心發生越界存取 (索引值超過陣列本身) 的問題,會直接導致 kernel panic 並卡死,並不像平時只會在執行時回報 segment fault,需要特別留意。 :::spoiler outdated ### 修正 Fib(93) 使用 gcc 實作的 `__uint128_t` 避免 64 位元整數的溢位,以及使用 `copy_to_user` kernel API 將計算完的 Fibonacci 數傳至 user space ### 輸出 (printf) 新增 `print_u128` 函式來印出計算後的 Fibonacci 數,將 128 位元的無號數對半拆成高、低位元部份處理 ---- :::warning 目前問題: Fib(12) 開始會出現亂碼 (之前都正常) ,應該是 `copy_to_user` 使用方式錯誤所導致 :::