--- tags: linux2022 --- # 2022q1 Homework3 (fibdrv) contributed by < `Xx-oX` > ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 43 bits physical, 48 bits virtual CPU(s): 8 On-line CPU(s) list: 0-7 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: AuthenticAMD CPU family: 23 Model: 17 Model name: AMD Ryzen 7 2700U with Radeon Vega Mobile Gfx Stepping: 0 Frequency boost: enabled CPU MHz: 1700.000 CPU max MHz: 2200.0000 CPU min MHz: 1600.0000 BogoMIPS: 4391.49 Virtualization: AMD-V L1d cache: 128 KiB L1i cache: 256 KiB L2 cache: 2 MiB L3 cache: 4 MiB NUMA node0 CPU(s): 0-7 ``` ## 回答==自我檢查清單== - [x] 研讀上述 ==Linux 效能分析==的提示 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 - [x] 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 clz / ctz 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說 - [x] 複習 C 語言 數值系統 和 bitwise operation,思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本 - [ ] 研讀 KYG-yaya573142 的報告,指出針對大數運算,有哪些加速運算和縮減記憶體操作成本的舉措? - [ ] lsmod 的輸出結果有一欄名為 Used by,這是 “each module’s use count and a list of referring modules”,但如何實作出來呢?模組間的相依性和實際使用次數 (reference counting) 在 Linux 核心如何追蹤呢? > 搭配閱讀 The Linux driver implementer’s API guide » Driver Basics - [ ] 注意到 `fibdrv.c` 存在著 `DEFINE_MUTEX`, `mutex_trylock`, `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 POSIX Thread 的程式碼來確認。 ## 效能分析設定 ### 實驗環境設定 > 參考 [Linux 效能分析的提示](https://hackmd.io/@sysprog/linux2022-fibdrv#-Linux-%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90%E7%9A%84%E6%8F%90%E7%A4%BA)以及 [KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU#%E6%8E%92%E9%99%A4%E5%B9%B2%E6%93%BE%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90%E7%9A%84%E5%9B%A0%E7%B4%A0) #### 空出編號 6, 7 的兩個核心 > 為何要空出兩個 => 一來預備給未來跑多執行緒或者其他狀況使用,二來不想跟別人一樣XD > 從實驗環境可以看到這顆 cpu 只有一個 NUMA node => 不用考慮共享記憶體的問題 修改 `/etc/default/grub` 檔案內的 `GRUB_CMDLINE_LINUX_DEFAULT` ```bash GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=6,7" ``` 然後執行 ```shell $ sudo update-grub ``` 更新設定,接著重新啟動 從 /sys/devices/system/cpu/isolated 中確認是否生效 ```shell $ cat /sys/devices/system/cpu/isolated 6-7 ``` #### 將測試程式固定在特定 cpu 上執行 > 這邊使用預留的 6 號 cpu 利用 [taskset](https://man7.org/linux/man-pages/man1/taskset.1.html) 指定特定行程的處理器親和性 (processor affinity) 將行程固定在特定 cpu 上執行 ```shell # 0x40 -> 0100 0000 -> 表示只使用第 6 個 cpu (從左而右為編號 7-0) # 啟動時指定 $ taskset 0x40 <EXECUTABLE> # 更改已經啟動的行程 $ taskset -p 0x40 <PID> # 查看特定行程的處理器親和性 $ taskset -p <PID> ``` #### 排出干擾分析的因素 * 抑制 [address space layout randomization (ASLR)](https://en.wikipedia.org/wiki/Address_space_layout_randomization) ```shell $ sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" ``` * 設定 [scaling_governor](https://www.kernel.org/doc/html/latest/admin-guide/pm/cpufreq.html) 為 performance > 設定 CPU 以最高頻率執行所有 process * 只對 6 號 cpu 進行設定 ```shell $ sudo sh -c "echo performance > /sys/devices/system/cpu/cpu6/cpufreq/scaling_governor" ``` * 設定全部 cpu 準備以下 shell script ```bash # performance.sh for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor do echo performance > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor done ``` ```shell $ sudo sh performance.sh ``` * 關閉 Intel 處理器的 turbo mode 使用之 AMD 處理器沒有這個東西 ### 記錄時間與繪圖 #### Kernel space 在 `fib_read` 中使用 [ktime API](https://www.kernel.org/doc/html/latest/core-api/timekeeping.html) 進行計時,並將結果輸出到 `kernel ring buffer` 中 ```c static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { ktime_t kt_start = ktime_get(); ssize_t result = fib_sequence(*offset); ktime_t duration = ktime_get() - kt_start; printk(KERN_DEBUG "%lld %lld", *offset, ktime_to_ns(duration)); return result; } ``` 使用 [dmesg](https://man7.org/linux/man-pages/man1/dmesg.1.html) 命令可以看到結果 > `sudo dmesg -c` 可以在顯示結果後清空 `kernel ring buffer` #### User space > 記錄之總時間 T = T(user space) + T(kernel space) 在 `client.c` 中使用 [clock_gettime](https://man7.org/linux/man-pages/man2/clock_getres.2.html) 進行計時,並將結果記錄以便繪圖 ```c FILE *fp = fopen(PATH, "w+"); if (!fp) { printf("Time record error!\n"); } for (int i = 0; i <= offset; i++) { clock_gettime(CLOCK_REALTIME, &ts_start); lseek(fd, i, SEEK_SET); sz = read(fd, buf, 1); clock_gettime(CLOCK_REALTIME, &ts_end); printf("Reading from " FIB_DEV " at offset %d, returned the sequence " "%lld.\n", i, sz); char tbuf[16]; snprintf(tbuf, 16, "%d %ld\n", i, ts_end.tv_nsec - ts_start.tv_nsec); fwrite(tbuf, sizeof(char), strlen(tbuf), fp); } fclose(fp); ``` #### 繪圖 使用 gnuplot 進行繪圖 ```bash reset set title 'Runtime comparison' set xlabel 'F(n)' set ylabel 'time(nsec)' set term png enhanced font 'Verdana,10' set output 'timeplot.png' set grid plot [0:][0:] \ './original' using 1:2 with linespoints linewidth 2 title 'iterative', \ './fast_doubling' using 1:2 with linespoints linewidth 2 title 'fast doubling' ``` ## 費式數列 > 改寫自[作業要求](https://hackmd.io/@sysprog/linux2022-fibdrv#-%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97) ### Scenario * 成年 (adult) 兔每個月生一隻小兔兔 * 小兔兔 (juvenile) 生長一個月後即成年 * 起初只有一隻小兔兔 * 兔子不會死 畫成表格 | Month | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | -------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | Juvenile | 1 | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | | Adult | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | | Total | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | 144 | 用 $a_n$ 表示成年兔, $j_n$ 表示小兔兔 $F_n$ 表示兔子總量,即 $a_n + j_n$ 又$F_n = a_{n+1}$ = $j_{n+2}$ 整理可得 $$ \left\{ \begin{array}\\ a_{n+1} = a_n + j_n\\ j_{n+1} = a_n\\ \end{array}\\ \right. $$ 寫成矩陣 $$ \begin{pmatrix} a_{n+1} \\ j_{n+1} \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix} \begin{pmatrix} a_n \\ j_n \end{pmatrix} $$ 即 $$ \begin{pmatrix} F_n \\ F_{n-1} \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix} \begin{pmatrix} F_{n-1} \\ F_{n-2} \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 0 & 0 \end{pmatrix}^{n-1} \begin{pmatrix} F_1 \\ F_0 \end{pmatrix} $$ 其中 $\begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}$ 又稱為 Q-matrix $$ Q = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix} = \begin{pmatrix} F_2 & F_1 \\ F_1 & F_0 \end{pmatrix} \\ Q^n = \begin{pmatrix} F_{n+1} & F_n \\ F_n & F_{n-1} \end{pmatrix} $$ ### Fast Doubling 帶入 $n:= 2n + 1$ $$ \begin{split} \begin{pmatrix} F_{2n+1} \\ F_{2n} \end{pmatrix} &= \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}^{2n} \begin{pmatrix} F_1 \\ F_0 \end{pmatrix}\\ \\ &= \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}^n \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}^n \begin{pmatrix} F_1 \\ F_0 \end{pmatrix}\\ \\ &= \begin{pmatrix} F_{n+1} & F_n \\ F_n & F_{n-1} \end{pmatrix} \begin{pmatrix} F_{n+1} & F_n \\ F_n & F_{n-1} \end{pmatrix} \begin{pmatrix} 1 \\ 0 \end{pmatrix}\\ \\ &= \begin{pmatrix} F_{n+1}^2 + F_n^2\\ F_nF_{n+1} + F_{n-1}F_n \end{pmatrix} \end{split} $$ 因此可得: $$ \begin{aligned} F_{2k} &= F_k ( 2F_{k+1} - F_k ) \\ F_{2k+1} &= F_{k+1}^2+F_k^2 \end{aligned} $$ 其中第一式即 Vorobev 等式 ### 分析 #### 利用定義計算 $F_n = F_{n-1} + F_{n-2}$ 以 $F(6)$ 為例: ```graphviz strict digraph G { 1[label="F(6)"] 2[label="F(4)"] 3[label="F(5)"] 4[label="F(2)"] 5[label="F(3)"] 6[label="F(3)"] 7[label="F(4)"] 8[label="F(0)", style=filled] 9[label="F(1)", style=filled] 10[label="F(1)", style=filled] 11[label="F(2)"] 12[label="F(1)", style=filled] 13[label="F(2)"] 14[label="F(2)"] 15[label="F(3)"] 16[label="F(0)", style=filled] 17[label="F(1)", style=filled] 18[label="F(0)", style=filled] 19[label="F(1)", style=filled] 20[label="F(0)", style=filled] 21[label="F(1)", style=filled] 22[label="F(1)", style=filled] 23[label="F(2)", style=filled] 24[label="F(0)", style=filled] 25[label="F(1)", style=filled] 1 -> {2, 3} 2 -> {4, 5} 3 -> {6, 7} 4 -> {8, 9} 5 -> {10, 11} 6 -> {12, 13} 7 -> {14, 15} 11 -> {16, 17} 13 -> {18, 19} 14 -> {20, 21} 15 -> {22, 23} 23 -> {24, 25} } ``` #### 利用 Fast Doubling 計算 一樣以 $F(6)$ 為例: ```graphviz strict digraph G { 1[label="F(6)"] 2[label="F(3)"] 3[label="F(4)"] 4[label="F(1)", style=filled] 5[label="F(2)", style=filled] 6[label="F(2)", style=filled] 7[label="F(3)"] 8[label="F(1)", style=filled] 9[label="F(2)", style=filled] 1 -> {2, 3} 2 -> {4, 5} 3 -> {6, 7} 7 -> {8, 9} } ``` 利用 $F_0 = 0, F_1 = 1, F_2 = 1$ 即可推導出隨後的數值,而且明顯比前面的遞迴定義要有效率。 ## 實作 `fibdrv` ### 使用 Fast Doubling 改進計算效率 將上述之 Fast Doubling 手法實作成程式碼 > 參考並使用 `__builtin_clz` 改寫 [Calculating Fibonacci Numbers by Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling) 一文中提及的實作 > int __builtin_clz (unsigned int x) > Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined. 加入 `unlikely` 巨集,使編譯器產生對分支預測友善的組合語言 >long __builtin_expect (long exp, long c) > You may use __builtin_expect to provide the compiler with branch prediction information. ... > The return value is the value of exp, which should be an integral expression. ```c #define unlikely(x) __builtin_expect(!!(x), 0) static long long fib_fast_doubling(long long k) { /* use fast doubling method */ // F(0) = 0, F(1) = 1, F(2) = 2 if (unlikely(k < 2)) return k; unsigned long long a = 0, b = 1; unsigned int i = 1u << (31 - __builtin_clz(k)); // find the highest set digit of k for (; i; i >>= 1) { unsigned long long c = a * (2 * b - a); // F(2n) = F(n) * [ 2 * F(n+1) – F(n) ] unsigned long long d = a * a + b * b; // F(2n+1) = F(n)^2 + F(n+1)^2 if (i & k) { // n_j is odd: n = (k_j-1)/2 => k_j = 2n + 1 a = d; // F(k_j) = F(2n+1) b = c + d; // F(k_j + 1) = F(2n + 2) = F(2n) + F(2n+1) } else { // k_j is even: n = k_j/2 => k_j = 2n a = c; // F(k_j) = F(2n) b = d; // F(k_j + 1) = F(2n + 1) } } return a; } ``` #### 實驗並比較計算效率 使用前述效能分析設定中的方法,使用 6 號 cpu 進行實驗 利用前述記錄時間的方法記錄採用 iterative 與 fast doubling 方法的時間 * Kernel time ![](https://i.imgur.com/rUXoDx8.png) 可以看出在 F(40) 之後兩種方法的時間差持續擴大 雖然只有算前 92 項,但大略可以看出 iterative 的時間複雜度約成 O(n),而 fast doubling 則比較偏向 O(1) * total (user + kernel) time ![](https://i.imgur.com/0LURyBl.png) 去掉過大的資料之後變成 ![](https://i.imgur.com/5kQOyUc.png) 比較 total time 與 kernel time 發現超過 50 項之後 total time 並沒有像 kernel time 所展現的兩種方法的時間差距越來越大 預計多作幾次實驗並採平均來降低單一個案的誤差 ### 計算第 93 項(含)之後的 Fibonacci 數 > Unfinished #### SSO (Small String Optimization) [參考資料](http://wdv4758h.github.io/notes/cpp/string-optimization.html) 實作資料結構如下 數字小的時候直接使用前 7 byte 來儲存 數字大的時候使用陣列,並記錄起始位置及長度 ```c /* * byte map: |----|----|----|----|----|----|----|----| * long ver: | ptr | length | * short ver: | number |flag| */ struct _long { unsigned long long *ptr; unsigned int length; }; struct _short { unsigned char number[sizeof(struct _long) - 1]; unsigned char flag; // 0x0 }; typedef union { struct _long; struct _short; } bn_t; ``` 運用於大數運算: 二元運算時須先判斷兩者的類型,過於麻煩,提昇之效率不符合比例原則。 #### 實作 採用類似字元陣列的方式儲存十進位的每位數字,只是將本來一個十進位數字 1 byte 的空間縮減成 1/2 byte > 1/2 byte => 4 bit 可表示 1~15 ,已經滿足需求 #### 資料結構 [測試 code](https://github.com/Xx-oX/BigNumber) 目前資料結構設計 ```c /* * Hadron: basic chunk for storing decimal digits * size: 4 bytes * |xx|xx|xx|xx| * |00|bt|sc|du| (little endian) * @u, d, c, s, t, b: for storing a decimal digit (0~9) * @p: last 8bits of next Hardon chunk's address (00 for null) * "Hadron": composite of "Quark" * Naming by particles of "Quark". */ typedef struct { unsigned char u : 4; //up unsigned char d : 4; //down unsigned char c : 4; //charm unsigned char s : 4; //strange unsigned char t : 4; //top unsigned char b : 4; //bottom unsigned char p; // pointer[-1: -4] } Hadron; /* * buint_t: variable size unsigned big integer * size: 16 bytes * @ptr: list of Hardons * @len: size of list(0~65535) * @addrm: address mask for concating Hadron.p * |xx|xx|xx|xx|xx|xx|xx|xx|xx|xx|xx|xx|xx|xx|xx|00| * | ptr | len | addrm | */ typedef struct{ Hadron *ptr; //start of Hadron list uint16_t len; //size of ptr[] uint64_t addrm : 48; //virtual memory address mask } buint_t; ``` > [Hadron](https://en.wikipedia.org/wiki/Hadron): 強子 > [Quark](https://en.wikipedia.org/wiki/Quark): 夸克,共有六種(即 u, d, c, s, t, b) > 剩下 1 byte padding 作為紀錄下一個區塊記憶體位置結尾 1 byte 利用 bitfield 創造 1/2 byte 的空間,用來儲存 0~9 的數字 每 6 個數字打包成一組 並使用結合 linked list 與區塊鏈的方式,每個區塊末端儲存下一個區塊的記憶體位置(結尾 1 byte) > Pros: 不用白不用,最後的 1 byte 也會因為 struct alignment 而被創造、計算記憶體使用簡單的 bitwise 運算 > Cons: 需要確保同個 buint_t 下的區塊記憶體位置的前 48 - 8 bits 必須一樣 => 自行實作記憶體管理 > 運行加法、乘法繁瑣 => 遲遲無法完成 #### 加法 * 運用直式加法的手法實作 #### 乘法 * 計畫使用 [Schönhage–Strassen 演算法](https://zh.wikipedia.org/wiki/Sch%C3%B6nhage-Strassen%E6%BC%94%E7%AE%97%E6%B3%95) * 但因為 [FFT](https://en.wikipedia.org/wiki/Fast_Fourier_transform) 實作及理解不易,預計拿掉使用 FFT 的部份,演變成使用線性卷積統一處理進位的簡易版本 ``` e.g. 123 * 456 = 56088 1 2 3 x) 4 5 6 --------------- 6 12 18 5 10 15 +) 4 8 12 -------------------- 4 13 28 27 18 計算卷積 18 27- 28-- 13--- +) 4---- --------- 56088...即為所求 ```