--- tags: linux2023 --- # 2023q1 Homework3 (fibdrv) contributed by <`DokiDokiPB` > :::danger 使用一個 grave accent 符號來標注,注意細節! :notes: jserv ::: #### 實驗環境 ```shell $ 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): 4 On-line CPU(s) list: 0-3 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-4250U CPU @ 1.30GHz Thread(s) per core: 2 Core(s) per socket: 2 Socket(s): 1 Stepping: 1 CPU max MHz: 2600.0000 CPU min MHz: 800.0000 Virtualization features: Virtualization: VT-x Caches (sum of all): L1d: 64 KiB (2 instances) L1i: 64 KiB (2 instances) L2: 512 KiB (2 instances) L3: 3 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-3 ``` 使用 Macbook Air 2013 A1466 (Ubuntu Linux) 二核四執行緒 :::warning 區分 processor core (核) 和 OS kernel (核心) :notes: jserv ::: 另外舊版的電腦沒有 Secure Boot 的問題,可以直接加載 fibonacci driver #### 設定 VSCode 開發環境 fibdrv 作業本身沒有把 .vscode 的目錄加入 .gitignore 列表中,需要手動新增 `.vscode` :::warning 可提交 pull request。 :notes: jserv ::: 當 VSCode 第一次開啟 workspace 時,會建立 `.vscode` 目錄跟 `c_cpp_properties.json` 檔案,修改該檔案,即可在 VSCode 中使用語法提示: ```json { "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**", "/usr/include", "/usr/local/include", "/usr/src/linux-headers-5.19.0-32-generic/include", "/usr/src/linux-headers-5.19.0-32-generic/arch/x86/include", "/usr/src/linux-headers-5.19.0-32-generic/arch/x86/include/generated", "/usr/lib/modules/5.19.0-32-generic/build/include", "/usr/lib/gcc/x86_64-linux-gnu/11/include" ], "defines": [ "KBUILD_MODNAME=\"fibdrv_new\"", "__GNUC__", "__KERNEL__", "MODULE", "_USERSPACEFIB" ], "compilerPath": "/usr/bin/gcc", "cStandard": "gnu17", "cppStandard": "gnu++17", "intelliSenseMode": "linux-gcc-x64" } ], "version": 4 } ``` `_USERSPACEFIB` 是自定義的巨集,在實作大數時,在使用者層級(userspace) 驗證每個函式正確性非常方便。 不同的設定為影響 VSCode 解析巨集時候的行為,例如 `"cStandard": "gnu17"`,若是設定成 `"cStandard": "c99"`,部份結構體如 `struct timespec` 就不會被 VSCode 解析 ### 自我檢查清單 - [ ] 研讀上述 Linux 效能分析的提示描述 - [ ] 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 clz / ctz 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說 - [ ] 複習 C 語言 數值系統 和 bitwise operation,思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本 - [ ] 學習指出針對大數運算的加速運算和縮減記憶體操作成本的舉措,紀錄你的認知和疑惑 - [ ] 注意到 fibdrv.c 存在著 `DEFINE_MUTEX`, `mutex_trylock`, `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢? # 開發紀錄 ##### 初步修改 `fib_sequence` 參考作業說明以及 [Fast Doubling 手法說明](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-a#-%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97) 1. $Fib(2k) = Fib(k)[2Fib(k+1) - Fib(k)]$ 2. $Fib(2k + 1) = Fib(k+1)^2 + Fib(k)^2$ 在以下程式碼中,分別代表變數: - $Fib(k) \rightarrow a$ - $Fib(k + 1) \rightarrow b$ ```c static long long fib_sequence(long long k) { unsigned long long mask = ULLONG_MAX ^ (ULLONG_MAX >> 1); mask >>= __builtin_clz(k); unsigned long long a = 0, b = 1; // fib(0), fib(1) for (; mask; mask >>= 1) { unsigned long long c = a * (2 * b - a); unsigned long long d = a * a + b * b; int aset = !(mask & k); int bset = !!(mask & k); a = d ^ ((c ^ d) & -aset); b = d + (c & -bset); } return a; } ``` 其中的 `aset` 與 `best` 的位元技巧改寫自以下程式碼: ```c if (mask & offset) { a = d; b = c + d; } else { a = c; b = d; } ``` 使用 `make load` 後使用 `sudo ./client` 輸出,可以計算到 $Fib(92)$ ``` Reading from /dev/fibonacci at offset 89, returned the sequence 1779979416004714189. Reading from /dev/fibonacci at offset 90, returned the sequence 2880067194370816120. Reading from /dev/fibonacci at offset 91, returned the sequence 4660046610375530309. Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429. Reading from /dev/fibonacci at offset 93, returned the sequence 7540113804746346429. ``` ## 使用 `bn` 結構體實作 Fibonacci 之前撰寫的 [2022 fibdrv](https://hackmd.io/@DokiDokiPB/2022fibdrv) 中使用的 ```decnum``` 程式碼,採用 $10^9$ 進制實作(在 $32$ 位元下),進位時需要用到除法運算子(```%```),撰寫程式碼上並沒有比較簡單;對比以 $2$ 進制實作的運算時間差異巨大,因此這次採用 $2$ 進制實作。 這裡採用 `_bn` 大數結構體來實作。新增 `bn.c`, `bn.h` 用於實作 `_bn`,並在 `Makefile` 修改名稱,使其可以將該新增檔案編譯 ```diff - TARGET_MODULE := fibdrv + TARGET_MODULE := fibdrv_new - $(TARGET_MODULE)-objs := fibdrv.o + $(TARGET_MODULE)-objs := fibdrv.o bn.o ``` <!-- 這裡實作上 - 參考 [yaya573142](https://github.com/KYG-yaya573142/fibdrv/blob/master/bn_kernel.c#L211) 中的作法 - 參考 [sysprog21/bignum](https://github.com/sysprog21/bignum),自行建立兩個檔案 `bn.h` 與 `bn.c` --> ### 使用 `ufib.c` 在使用者層級進行測試 - 加載費氏數列模組進入 Linux kernel 後,就會很難除錯,很多時候若是實作的函式本身若寫錯,需要花費多餘的時間檢查。 - 引入 `bn` 結構體進 `fibdrv` 模組,相關測試程式碼也需要加入的。 雖然 C 語言一定有測試的函式庫可以使用,這裡只單獨新增 `ufib.c` 的檔案測試。新增一個在使用者層級即可編譯與偵錯的程式碼與巨集 在 `bn.h` 中新增巨集 `_USERSPACEFIB`,用於在使用者層級測試程式碼用。 ```c #ifndef _USERSPACEFIB // ... #else #include <stdbool.h> #include <stdlib.h> #include <string.h> #define kmalloc(size, flags) (malloc(size)) #define krealloc(ptr, size, flags) (realloc((ptr), (size))) #define kfree(p) (free(p)) typedef unsigned int __u32; typedef signed int __s32; typedef unsigned long __u64; typedef signed long __s64; #define GFP_KERNEL (0) #endif ``` 搭配檔案 `ufib.c`,並在 Makefile 中新增 `$(CC) -g ufib.c bn.c -o ufib.out -D _USERSPACEFIB` 即可編譯對應檔案 ### _bn 結構體 ```c typedef struct _bn { __u32 size; __u32 *num; } bn; void bn_add(bn *a, bn *b, bn *c); void bn_diff(bn *a, bn *b, bn *c); void bn_lshift(bn *a, __u32 s); void bn_mult(bn *a, bn *b, bn *c); #define BN_DIV_ROUND(x, len) (((x) + (len) - 1) / (len)) ``` - `size` 即表示配置位元組的大小,在實作上,結構體不一定用到全部的位元組。 - `BN_DIV_ROUND` 實作上與 `DIV_ROUND` 相同 透過 `BN_DIV_ROUND` 巨集 $\lceil \frac{digits + 31}{32} \rceil$ 計算需要的位元組,以數值列出關係 ``` digits macro 0 (0 + 32 - 1) / 32 = 0 1 (1 + 32 - 1) / 32 = 1 2 (2 + 32 - 1) / 32 = 1 32 (32 + 32 - 1) / 32 = 1 ...... 33 (33 + 32 - 1) / 32 = 2 64 (64 + 32 - 1) / 32 = 2 ``` #### bn_add 函式 ```c void bn_add(bn *a, bn *b, bn *c) { __u32 digits = BN_MAX(bn_msb(a), bn_msb(b)) + 1; digits = BN_DIV_ROUND(digits, 32) + !digits; bn_resize(c, digits); bn aa = *a; bn bb = *b; if (a->size < b->size) bn_swap(&aa, &bb); __u64 carry = 0; __u32 i = 0; for (; i < b->size; i++) { carry += (__u64) aa.num[i] + (__u64) bb.num[i]; c->num[i] = carry; carry >>= 32; } for (; i < a->size; i++) { carry += a->num[i]; c->num[i] = carry; carry >>= 32; } if (carry) { c->num[i] = carry; } } ``` 兩數相加最多只會進 $1$ 位,因此 `*c` 需要配置與 `*a` 或 `*b` 中最大位元數量 $+1$。 其中若是 `*a == *c` 或 `*b == *c`,使用 `*aa` 與 `*bb` 代為操作加法。 #### bn_diff 函式 ```c void bn_diff(bn *a, bn *b, bn *c) { int abcmp = bn_cmp(a, b); if (abcmp == 0) { bn_set_zero(c); return; } bn aa = *a; bn bb = *b; if (abcmp == 1) bn_swap(&aa, &bb); bn_resize(c, a->size); __u64 carry = 0; __u32 i = 0; for (; i < bb.size; i++) { __u64 x = (__u64) aa.num[i] - (__u64) bb.num[i] - carry; __u64 sign = !!(x >> 32); c->num[i] = x + (sign << 32); carry = sign; } for (; i < aa.size; i++) { __u64 x = (__u64) aa.num[i] - carry; __u64 sign = !!(x >> 32); c->num[i] = x + (sign << 32); carry = sign; } } ``` 在 fast doubling 手法中,減法運算皆為較大的數字減去較小的數字,因此實作上可以改為計算兩者的差值。 1. `bn_cmp` 決定和者數字較大,由大減去小的數字,用來保證是大數減去小數字。 2. 在計算減法過程中,若是某個位數減出負數,則向下一位位數借位。 - 負數在 $2$ 補數系統中,變數 `x` 的位元從 `[63:32]` 皆為 $1$ ,透過 `sign = !!(x >> 32)` 即可判斷正負。並加回一個 `sign << 32`,`carry` 向高位元借位。 過程上幾乎與 `bn_add` 函式相同 #### bn_mult 函式 ```c void bn_mult(bn *a, bn *b, bn *c) { __u32 digits = bn_msb(a) + bn_msb(b) + 1; digits = BN_DIV_ROUND(digits, 32) + !digits; bn_resize(c, digits); if (!c->num) return; bn_set_zero(c); for (__u32 i = 0; i < a->size; i++) { for (__u32 j = 0; j < b->size; j++) { __u64 carry = (__u64) a->num[i] * (__u64) b->num[j]; __u32 k = i + j; do { __u64 s = carry + c->num[k]; c->num[k] = s; carry = s >> 32; k++; } while (k < c->size && carry); } } } ``` 與加法、減法不同之處,在於某兩位數乘法之後,至多指派兩組資料到不同記憶體空間,因此必須是 `a != c` 且 `b !!= c` #### bn_lshift 函式 ```c void bn_lshift(bn *a, __u32 s) { s = s & 0b11111; __u64 carry = 0; __u32 i = 0; for (; i < a->size; i++) { __u64 x = a->num[i]; a->num[i] = (x << s) | carry; carry = x >> (32 - s); } if (carry) { bn_resize(a, i + 1); a->num[i] = carry; } } ``` 最大左移量為 31,在 fast doubleing 手法中只須左移 1,也就是乘以 2 #### `bn_clz` 函式與 `bn_msb` 函式 初步的實作從最高位數去計算零的數量 ```c static __u32 bn_clz(const bn *p) { __u32 cnt = 0; for (__s64 i = p->size - 1; i >= 0; i--) { if (p->num[i]) { cnt += __builtin_clz(p->num[i]); break; } else { cnt += 32; } } return cnt; } static __u32 bn_msb(const bn *p) { return (p->size << 5) - bn_clz(p); } ``` ### 實作加法的 Fibonacci 數列 該函式實作於 `ufib.c` 中 ```c static void test_bn_add(__u64 k) { bn a, b, c; /* ... */ a.num[0] = 0; b.num[0] = 1; for (__u64 i = 0; i < k; i++) { bn_add(&a, &b, &c); bn_swap(&a, &b); bn_swap(&c, &b); } /* the result is stored in *a */ } ``` ### 實作 Fast Doubling 手法的 Fibonacci 數列 該函式實作於 `ufib.c` 中 ```c static void test_bn_fast_doubling(__u64 k) { __u64 mask = 1UL << 63; mask >>= __builtin_clzl(k); bn a, b, c, d; /* ... */ a.num[0] = 0; b.num[0] = 1; for (; mask; mask >>= 1) { bn_cpy(&d, &b); bn_lshift(&d, 1); bn_diff(&d, &a, &d); bn_mult(&a, &d, &c); bn_mult(&a, &a, &d); bn_mult(&b, &b, &a); bn_add(&d, &a, &d); if (mask & k) { bn_add(&c, &d, &b); bn_swap(&a, &d); } else { bn_swap(&c, &a); bn_swap(&d, &b); } } } ``` 觀摩 [yanjiew 同學](https://hackmd.io/@yanjiew/linux2023q1-fibdrv#%E5%AF%A6%E4%BD%9C%E5%A4%A7%E6%95%B8%E9%81%8B%E7%AE%97%E7%89%88%E6%9C%AC%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97)底下留言中可以使用 Hash table,來嘗試引入課堂中提到的 Hash table #### 將大數轉換成字串 這裡直接使用 [yaya573142 同學](https://github.com/KYG-yaya573142/fibdrv/blob/master/bn_kernel.c#L47)的 `bn_to_string` 令 $n$ 表示十進制的費式數列的結果,最終需要長度為 $y = \lceil log_{10}{n} \rceil$ 的字串,則 - 可化為 $y = \lceil \frac{log_2{n}}{log_2{10}} \rceil$ - $log_{2}{n}$ 即表示最高位有效位元位置,可藉由 `bn_msb` 或簡單的 `8 * sizeof(__u32) * size` 求出 - $log_{2}{10}$ 約略為 $3.222 \approx 3$ - `y = (8 * sizeof(__u32) * size) / 3 + 3` ## 加入量測時間 ### 在使用者層級量測時間 先直接在 `ufib.c` 上直接進行測試加入 `ufib_plot.gp` 檔案 ``` plot [0:500][0:] \ "ufib_time.ut" using 1:2 with linespoints linewidth 2 pointtype 7 pointsize 0.5 title "fast doubling" ``` 對應程式碼: ```c #include <time.h> #define NANOSECOND(x) ((x).tv_sec * 1e9 + (x).tv_nsec) struct timespec t1, t2; for (int i = 0; i < 500; i++) { clock_gettime(CLOCK_MONOTONIC, &t1); clock_gettime(CLOCK_MONOTONIC, &t2); long long ut = (long long) (NANOSECOND(t2) - NANOSECOND(t1)); printf("%d %lld\n", i, ut); } ``` Makefile 新增 ``` uplot: ufib ./ufib.out > ./ufib_time.ut gnuplot scripts/ufib_plot.gp ``` 即可產生圖片 ![](https://i.imgur.com/o2fs6E5.png) 忽略極端值之後,從 1000 ns 開始往上遞增,最終 $Fib(500)$ 為 5220 ns 對比 2022 年寫的 fibdrv 採用 $10^9$ 進制的執行時間差異巨大,起始時間從 10000 ns 開始,有明顯的提昇 :+1: <!-- ### 改進實作 主要可以觀察到兩種改進路線 - 針對記憶體如 `kmalloc` 改進 - 修改演算法,修改乘法方式或引入平方計算 --> ### 將 bn 結構體加入 `fibdrv.c` 中 > git commit [41c651ec143d6e606e088e3034098d52ae458e1a](https://github.com/a1091150/fibdrv_2023/commit/41c651ec143d6e606e088e3034098d52ae458e1a) 加入程式碼之後,主要修改 `#define MAX_LENGTH 500` 與 `fib_read` 函式,設計成: - 將輸出字串結果透過 `copy_to_user` 複製回 user space - 將回傳值作為時間間隔 ```c static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { ktime_t kt; kt = ktime_get(); bn ret = fib_fast_doubling(*offset); kt = ktime_sub(ktime_get(), kt); if (!ret.num) goto end; char *s = bn_to_string(&ret); size_t len = strlen(s) + 1; len = len > size ? size : len; copy_to_user(buf, s, len); kfree(s); end: return (ssize_t) ktime_to_ns(kt); } ``` 新增 `tclient.c` 與 `scripts/kfib_plot.gp`,內容與 `ufib_plot.gp` 類似,最終產生結果 在圖片中某些測量極端數值,是因為尚未移除干擾。在[作業說明](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-c)中,需要移除干擾,使得結果穩定。 ![](https://i.imgur.com/dkkkpzp.png) ## 初步使用 `perf` 進行量測 在之前的 2022 fibdrv 中,使用 `ftrace` 與 `trace-cmd` 追蹤程式碼在 kernel space 中的行為,大致在 user space 下測試差不多。 使用 `sudo perf stat -r 2 -e cycles,instructions,cache-misses,cache-references,branch-instructions,branch-misses ./ufib.out`, `sudo perf report --stdio` 觀察結果 ### 測試 `test_fast_doubling` 函式 在 `ufib.c` 中的改為測試 `test_fast_doubling`,程式碼主體改為以下程式碼 ```c for (int i = 0; i < 500; i++) { test_bn_fast_doubling(500); } ``` ``` test_bn_fast_doubling |--44.39%--bn_mult | |--11.18%--bn_resize | |--3.75%--asm_sysvec_apic_timer_interrupt ... | --3.73%--bn_msb | |--14.86%--bn_add | --3.73%--bn_msb |--10.94%--bn_diff | --3.52%--bn_cmp, bn_msb, bn_clz |--7.58%--bn_new |--3.74%--bn_cpy --3.74%--bn_resize ``` ``` 7,793,228 cycles ( +- 2.18% ) 17,076,298 instructions # 2.26 insn per cycle ( +- 0.02% ) 4,560 cache-misses # 23.547 % of all cache refs ( +- 16.46% ) 18,457 cache-references ( +- 2.24% ) 2,121,315 branch-instructions ( +- 0.02% ) <not counted> branch-misses (0.00%) 0.0038004 +- 0.0000351 seconds time elapsed ( +- 0.92% ) ``` 觀察輸出,初步有兩種改進方向: - 改善記憶體配置時機跟大小: - `bn_resize` 有相當的比例 - 程式法部份的改進: - 意外的事是 `bn_add`, `bn_diff`, `bn_msb` 總共有 24 % - `bn_mult` 本身的乘法計算就佔去 30 % ### 測試 `test_bn_add` 函式 在 `ufib.c` 中的改為測試 `test_bn_add`,程式碼主體改為以下程式碼 ```c for (int i = 0; i < 500; i++) { test_bn_add(500); } ``` `perf` 輸出: ``` |--96.56%--test_bn_add | |--87.57%--bn_add | | |--18.25%--bn_msb | | | --10.87%--bn_clz | | --6.35%--bn_resize | |--4.48%--bn_swap | |--1.79%--bn_resize | --0.88%--bn_msb --0.90%--bn_swap ``` 使用 `sudo perf stat -r 10 -e cycles,instructions,cache-misses,cache-references,branch-instructions,branch-misses ./ufib.out` ``` Performance counter stats for './ufib.out' (10 runs): 66,434,477 cycles ( +- 0.64% ) (64.59%) 148,983,157 instructions # 2.23 insn per cycle ( +- 0.15% ) (84.58%) 5,870 cache-misses # 21.757 % of all cache refs ( +- 16.48% ) (84.58%) 24,720 cache-references ( +- 1.87% ) (84.58%) 14,859,034 branch-instructions ( +- 0.12% ) (84.58%) 32,670 branch-misses # 0.22% of all branches ( +- 2.83% ) (81.68%) 0.026891 +- 0.000233 seconds time elapsed ( +- 0.87% ) ``` `cache-misse` 在第一次執行程式時佔全部的 21 %,在第二次執行時會剩下 5 % 在加法程式碼實作中,目前看起來是沒有更多改進空間 <!-- https://hackmd.io/@KYWeng/rkGdultSU#%E6%94%B9%E5%96%84-bn_add-%E7%9A%84%E6%95%88%E8%83%BD -->