# 2022q1 Homework3 (fibdrv) contributed by < `rickywu0421` > > [作業說明](https://hackmd.io/@sysprog/linux2022-fibdrv#-作業要求) > [GitHub](https://github.com/rickywu0421/fibdrv) ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.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): 6 On-line CPU(s) list: 0-5 Thread(s) per core: 1 Core(s) per socket: 6 Socket(s): 1 NUMA node(s): 1 Vendor ID: AuthenticAMD CPU family: 23 Model: 113 Model name: AMD Ryzen 5 3500X 6-Core Processor Stepping: 0 Frequency boost: enabled CPU MHz: 2200.000 CPU max MHz: 4120.3120 CPU min MHz: 2200.0000 BogoMIPS: 7200.03 Virtualization: AMD-V L1d cache: 192 KiB L1i cache: 192 KiB L2 cache: 3 MiB L3 cache: 32 MiB NUMA node0 CPU(s): 0-5 ``` ## Fast Doubling 用 Fast doubling 的手段可以使計算 Fibonacci number 的時間複雜度降為 $O(logn)$。 詳細的推導過程請參考 [Fibdrv Fast Doubling](https://hackmd.io/@sysprog/linux2022-fibdrv)。 套用以下公式: $$ \begin{split} \left\{ \begin{array}{l} F_{2n+1} &= F_{n+1}^2 + F_n^2 \\ F_{2n} &= F_n\ (2 F_{n+1} - F_{n}) \end{array} \right. \end{split} $$ 對應的虛擬碼如下: ``` 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; ``` 以上的 "number of binary digit in n" 意思是 n 的 most significant set bit, 也就是 $log_2n$ 的值。 ## Find most significant set bit by $log_2n$ 上述演算法中, 為了得到 most significant set bit, 我們需要計算 $log_2n$, 而此函式的執行效率將顯著的影響程式執行效能, 尤其在 n 的值較小時。 以下實驗之 data model 為 LP64, 故 `unsigned long` 之資料位元數為 64。且實驗假設不會出現 `n == 0` 的情況。 ### Brute Force 由 MSB 迭代至 LSB, 找到第一個 set bit 時停止。 ```c int log2(unsigned long n) { int i; for (i = 63; i >= 0; i--) if (n & (1 << i)) break; return i; } ``` ### Count Leading Zeros 其實計算 $log_2n$ 等於先算出最高位有幾個 0 (leading zeros), 再用 63 扣掉就好。 故原本的 $log_2n$ 可以改寫為: ```c #define log2(n) \ (8 * sizeof(unsigned long) - clz(n) - 1) ``` #### clz 版本 1: iteration ## 修正 Fibonacci 數的計算 ### 定義大數結構體 - ubign 原始程式碼以 `long long` 型態的變數儲存 fibonacci number, 其計算到 f(93) 時會發生 integer overflow。 為了計算更高的 fibonacci number, 我定義了以下結構體: ```c typedef uint32_t ubn_b_t; typedef struct _ubn { ubn_b_t arr[UBN_ARRAY_SIZE]; } ubn_t; ``` 該結構體內部的成員為 `uint32_t` 的陣列, 陣列的大小 (`UBN_ARRAY_SIZE`) 可以根據要計算的 fibonacci number 大小進行調整。若要計算到 f(1000), 則 `UBN_ARRAY_SIZE` 必須至少為 24, 也就是用 96 bytes 的空間儲存 fibonacci number。 選擇 `uint32_t` 作為陣列元素型態而不使用 `uint64_t` 的原因為:在進行大數運算時 (add, sub, mul...), 可以使用 `uint64_t` integer 作為儲存運算結果的變數。 若使用 `uint64_t` 作為陣列元素型態, 則需要用 128 bit integer 儲存運算結果, 但其並非為編譯器預設提供的型態。(gcc 有提供 built-in type `__int128`) 該結構體儲存大數的位元組順序為 little endian。以下為一 96 bit integer 儲存到 `ubn_t` 的示意圖: $\underbrace{0111...11}_{32bit}\underbrace{1101...01}_{32bit}\underbrace{0010...10}_{32bit} => \underbrace{0010...10}_{arr[0]}, \underbrace{1101...01}_{arr[1]}, \underbrace{0111...11}_{arr[2]}$ 範例中的 `arr[3]` 到 `arr[UBN_ARRAY_SIZE - 1]` 則會被 zero padding。 ### 基本運算操作 以下操作的函式中, 傳入的 `ubn_t` 均為指標型態, 避免 copy 變數造成的空間及時間浪費。 #### ubn_from_extend 將 `ubn_b_extend_t` 變數儲存到 `ubn_t` 中。 ```c typedef uint64_t ubn_b_extend_t; #define UBN_BLOCK_SIZE sizeof(ubn_b_t) static inline void ubn_from_extend(ubn_t *ubn, ubn_b_extend_t tmp) { ubn_init(ubn); ubn->arr[0] = tmp; ubn->arr[1] = tmp >> (8 * UBN_BLOCK_SIZE); } ``` #### ubn_add 如何判斷是否 carry:相加後的數 < 被加數 ```c static void ubn_add(ubn_t *a, ubn_t *b, ubn_t *c) { int carry = 0; for (int i = 0; i < UBN_ARRAY_SIZE; i++) { ubn_b_t tmp_a = a->arr[i]; c->arr[i] = a->arr[i] + b->arr[i] + carry; carry = (c->arr[i] < tmp_a); } } ``` #### ubn_sub 如何判斷是否 borrow:相減後的數 > 被減數 ```c static void ubn_sub(ubn_t *a, ubn_t *b, ubn_t *c) { int borrow = 0; for (int i = 0; i < UBN_ARRAY_SIZE; i++) { ubn_b_t tmp_a = a->arr[i]; c->arr[i] = a->arr[i] - b->arr[i] - borrow; borrow = (c->arr[i] > tmp_a); } } ``` #### ubn_lshift_b 用於乘法操作, 將 `ubn_t` left shift 若干個 block, 並在低位元組的地方補 0。 ```c void ubn_lshift_b(ubn_t *ubn, int block) { for (int i = UBN_ARRAY_SIZE - 1; i >= block; i--) ubn->arr[i] = ubn->arr[i - block]; /* Zero padding */ memset(ubn->arr, 0, UBN_BLOCK_SIZE * block); } ``` #### ubn_mul 計算 `i` 個偏移量的 `a` 乘上 `j` 個偏移量的 `b`, 將結果放到 `ubn_t`, 並 left shift (`i + j`) 個偏移量, 再加到 `c`。 ```c void ubn_mul(ubn_t *a, ubn_t *b, ubn_t *c) { ubn_init(c); for (int i = 0; i < UBN_ARRAY_SIZE; i++) { for (int j = 0; j < UBN_ARRAY_SIZE; j++) { if ((i + j) < UBN_ARRAY_SIZE) { ubn_t ubn; ubn_b_extend_t tmp = (ubn_b_extend_t) a->arr[i] * b->arr[j]; ubn_from_extend(&ubn, tmp); ubn_lshift_b(&ubn, i + j); ubn_add(&ubn, c, c); } } } } ``` #### ubn_to_str 參考 [blueskyson](https://hackmd.io/OfRzBJjGRjGiHM_j-LGoRg?both) 的作法: 從 `ubn_t` 的最高為 1 的位元起, 1. 往 LSB 的方向逐個位元檢查是否為 1, 若是, 則 buf 中的數 + 1。 2. 將 buf 中的每個值加上自己, 並處理進位問題, 即可達成概念上的乘 2。 3. 重複步驟 1, 直到所有位元都已被檢查過 ```c void ubn_to_str(ubn_t *ubn, char *buf) { buf_init(buf); /* Skip zero block */ int index; for (index = UBN_ARRAY_SIZE - 1; !ubn->arr[index]; index--) ; for (; index >= 0; index--) { for (ubn_b_t mask = 1U << ((BITS_PER_BYTE * UBN_BLOCK_SIZE) - 1); mask; mask >>= 1U) { int carry = ((ubn->arr[index] & mask) != 0); for (int i = UBN_STR_SIZE - 2; i >= 0; i--) { buf[i] += buf[i] + carry - '0'; carry = (buf[i] > '9'); if (carry) buf[i] -= 10; } } } buf[UBN_STR_SIZE - 1] = '\0'; /* Eliminate leading zeros in buf */ int offset; for (offset = 0; (offset < UBN_STR_SIZE - 2) && (buf[offset] == '0'); offset++) ; int i; for (i = 0; i < (UBN_STR_SIZE - 1 - offset); i++) buf[i] = buf[i + offset]; buf[i] = '\0'; } ``` #### fib_sequence 透過 [fast dobuling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling) 的方式計算 fibonacci number。 我使用 `__builtin_clzll(n)` 先找到最高被 set 的位元, 接著 skip 這個位元, 從下一個位元開始計算, 如此便能將起始條件從 f(0) 改為 f(1), 省去一次 loop 的運算。 ```c #define flsll(n) (64 - __builtin_clzll(n)) static ubn_t fib_sequence(long long n) { ubn_t a, b, c, d; if (!n) { ubn_from_extend(&a, (ubn_b_extend_t) 0); return a; } if (n == 1) { ubn_from_extend(&a, (ubn_b_extend_t) 1); return a; } /* Starting from f(1), skip the most significant 1-bit */ int i = (flsll(n) - 1) - 1; ubn_from_extend(&a, (ubn_b_extend_t) 1); /* f(1) = 1 */ ubn_from_extend(&b, (ubn_b_extend_t) 1); /* f(2) = 1 */ for (int mask = 1 << i; mask; mask >>= 1) { ubn_t tmp1, tmp2, tmp3; ubn_from_extend(&tmp1, (ubn_b_extend_t) 2); /* tmp1 = 2 */ ubn_mul(&tmp1, &b, &tmp2); /* tmp2 = 2 * b */ ubn_sub(&tmp2, &a, &tmp3); /* tmp3 = 2 * b - a */ ubn_mul(&a, &tmp3, &c); /* c = a * (2 * b - a) */ ubn_mul(&a, &a, &tmp1); /* tmp1 = a * a */ ubn_mul(&b, &b, &tmp2); /* tmp2 = b * b */ ubn_add(&tmp1, &tmp2, &d); /* d = a * a + b * b */ if (n & mask) { a = d; ubn_add(&c, &d, &b); /* f(2k+2) = f(2k) + f(2k+1) = c + d */ } else { a = c; b = d; } } return a; } ``` ### copy_to_user 原始程式碼使用 read 的回傳值當做 fibonacci number, 其回傳的形態為 `ssize_t`, 翻閱 [include/linux/types.h](https://elixir.bootlin.com/linux/latest/source/include/linux/types.h): ```c typedef __kernel_ssize_t ssize_t; ``` 再翻閱 [/include/uapi/asm-generic/posix_types.h](https://elixir.bootlin.com/linux/latest/source/include/uapi/asm-generic/posix_types.h#L69) ```c typedef long __kernel_long_t; #if __BITS_PER_LONG != 64 typedef int __kernel_ssize_t; #else typedef __kernel_long_t __kernel_ssize_t; #endif ``` 得知, 在 64 bit 的機器下, `ssize_t` 等同於 64 bit signed integer, 其能容納的 fibonacci number 最高為 92。 於是改用 `copy_to_user()` 將 `ubn_t` 轉換成字串, 再從 kernel space 複製到 user space。 ```c /** * @buf: user space buffer * @str: fibonacci number * @len: length of @str */ copy_to_user((void *) buf, str, len); ``` ## 效能分析 ### 在 kernel space 計算 fibdrv 執行時間 透過 [include/linux/ktime.h](https://elixir.bootlin.com/linux/latest/source/include/linux/ktime.h) 及 [include/linux/timekeeping.h](https://elixir.bootlin.com/linux/latest/source/include/linux/timekeeping.h) 中定義的 `ktime_get()` , `ktime_sub()` 及`ktime_to_ns()` 達成計時的效果。 #### 記錄計算 fibonacci number 之花費時間 ```c #include <linux/ktime.h> ... kt_fib = ktime_get(); ubn_t fib = fib_sequence(*offset); ubn_to_str(&fib, str); kt_fib = ktime_sub(ktime_get(), kt_fib); kt_fib_long = ktime_to_ns(kt_fib); ``` #### 記錄 copy_to_user 之花費時間 ```c kt_copy = ktime_get(); ret = copy_to_user((void *) buf, str, len); kt_copy = ktime_sub(ktime_get(), kt_copy); kt_copy_long = ktime_to_ns(kt_copy); ``` ### 在 user space 計算 fibdrv 執行時間 透過 `clock_gettime()` 得到起始及結束時間 (以`struct timespec` 表示) ```c #include <time.h> ... clock_gettime(CLOCK_MONOTONIC, &start); ret = read(fd, buf, UBN_STR_SIZE); clock_gettime(CLOCK_MONOTONIC, &end); ``` `struct timespec` 之定義如下: ```c struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; ``` 透過以下操作可得到起始到結束的經過時間: ```c struct timespec diff; diff.tv_sec = end.tv_sec - start.tv_sec; diff.tv_nsec = end.tv_nsec - start.tv_nsec; long long elapsed_time = diff.tv_nsec + diff.tv_sec * 1000000; ``` ### 以統計手法去除極端值 假設數據分佈接近常態分佈的情況下,一個標準差到三個標準差之內的資料量約佔 68%, 95%, 99.7%: ![](https://i.imgur.com/chp2BHr.png) > 圖片來源:[wikipedia](https://en.wikipedia.org/wiki/Standard_deviation) #### python script 實作 使用 python script: [driver.py](https://github.com/rickywu0421/fibdrv/blob/master/scripts/driver.py) 執行若干次使用者程式蒐集數據, 並去除距離平均兩個標準差以上的數據。 > 參考自 [4ce43c4](https://github.com/colinyoyo26/fibdrv/commit/4ce43c4e7ee0b80c4ce9e6dcd995bbfdf239206c) `data` 為跑若干次程式中計算的某個 fibonacci number 的時間, 此函式回傳 `data` 中距離平均小於 2 個標準差的數據: ```python def outlier_filter(data, threshold=2): data = np.array(data) z = np.abs((data - data.mean()) / data.std()) return data[z < threshold] ``` 處理計算每項計算的時間 (kernel, user, `copy_to_user`), 將資料去除 outlier 後回傳過濾過的資料: ```python def data_processing(data, n): catgories = data[0].shape[0] samples = data[0].shape[1] final = np.zeros((catgories, samples)) for c in range(catgories): for s in range(samples): final[c][s] = \ outlier_filter([data[i][c][s] for i in range(n)]).mean() return final ``` ### 計算 fibnocci number 作圖 (version 1) 以下是計算 0 到 1000 的 fibonacci number 之結果: - 數據取樣 50 次, 並去除 outlier ![](https://i.imgur.com/CFoBsHd.png) 我們根據上圖可以觀察出, n ~= 50 時有微幅的波動。 ### 設定 process 的 cpu affinity 透過 taskset 指令, 搭配 coremask (0x1), 將 process 固定在 cpu0 上執行: ```bash taskset 0x1 ./client ``` ### 限定 CPU 給此程式使用 taskset 可指定 process 給特定的 CPU 使用, 但不代表其他 process 不會佔用指定的 CPU。設定的方式有兩種,一個是在開機時使用 boot loader 所提供的自訂開機參數功能,手動加入 `isolcpus=cpu_id` 參數,或是直接加在 GRUB 的設定檔中,這樣 Linux 的排程器在預設的狀況下就不會將任何一般行程放在這個被保留的 CPU 核心中執行,只有那些被 taskset 特別指定的行程可使用這些 CPU 核心。 修改 GRUB 設定檔 /etc/default/grub: ```bash GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=0" ``` 並重開機以生效。 ### 排除干擾效能分析的因素 - 抑制 [address space layout randomization](https://en.wikipedia.org/wiki/Address_space_layout_randomization) (ASLR) ```bash sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" ``` - 設定 cpu0 的 scaling_governor 為 performance ```bash sudo sh -c "echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor" ``` ### 計算 fibnocci number 作圖 (version 2) 一樣是計算 0 到 1000 的 fibonacci number 之結果: - 數據取樣 50 次, 並去除 outlier ![](https://i.imgur.com/jyYYlNj.png) 相較 version 1, 此次執行的結果擺脫了 n ~= 50 時的波動。