--- title: 2022q1 Homework3 (fibdrv) description: Linux 核心設計作業 fibonacci character device --- ###### tags: `Linux 核心設計 2022` # 2022q1 Homework3 (fibdrv) contributed by < [DokiDokiPB](https://github.com/a1091150/fibdrv) > ### 實驗環境 ``` Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 20 On-line CPU(s) list: 0-19 Thread(s) per core: 1 Core(s) per socket: 14 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 154 Model name: 12th Gen Intel(R) Core(TM) i7-12700H Stepping: 3 CPU MHz: 612.719 CPU max MHz: 6000.0000 CPU min MHz: 400.0000 BogoMIPS: 5376.00 Virtualization: VT-x L1d cache: 336 KiB L1i cache: 224 KiB L2 cache: 8.8 MiB NUMA node0 CPU(s): 0-19 ``` 附註: intel 第 12 代 CPU 可以關閉 E Core ## fibonacci 實作 <!-- ### 基本的實作 ```c long long fibseq_basic(long long offset) { if (!offset) { return 0; } long long y = 1, z = 1; for (int i = 2; i < offset; i++) { long long tmp = y; y = z; z = y + tmp; } return z; }; ``` --> ### fast doubling method 在 [Calculating Fibonacci Numbers by Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling) 網誌中,提及 fast doubling 手法 1. $Fib(2k) = Fib(k)[2Fib(k+1) - Fib(k)]$ 2. $Fib(2k + 1) = Fib(k+1)^2 + Fib(k)^2$ 例如 $Fib(10)$ 的計算過程,以 top-down 方法去觀察 $Fib(10) \rightarrow Fib(5) \rightarrow Fib(2) \rightarrow Fib(1) \rightarrow Fib(0)$ 使用第一第二計算方法是取決當下 $Fib(n)$ 的 n 是否為偶數 - $Fib(10) \rightarrow Fib(5)$,使用規則一 - $Fib(5) \rightarrow Fib(2)$,使用規則二 - $Fib(2) \rightarrow Fib(1)$,使用規則一 - $Fib(1) \rightarrow Fib(0)$,使用規則二 使用 bottom-up 手法去觀察其對應關係 > 乘 2 即左移一項 k << 1,因此可以理解為從 F(0) 開始,經過以下 4 個步驟可以得到 F(10),這也是為何 fast doubling 實作上會根據 bit 來決定計算的步驟 (0000 << 1) + 1 = 0001 (0001 << 1) + 0 = 0010 (0010 << 1) + 1 = 0101 (0101 << 1) + 0 = 1010 > > 引用自 [KYG-yaya573142 作者](https://hackmd.io/BZjwYw1FQ1O0NPY5kIS7VQ?view#fast-doubling) | i | start | 4 | 3 | 2 | 1 | result | |---|-------|----------|----------|----------|---------|--------| | n | - | ==1==010 | 1==0==10 | 10==1==0 | 101==0== | - | |F(m) | F(0) | F(0*2+1) | F(1*2) | F(2*2+1) | F(5*2) | F(10) | | a | 0 | 1 | 1 | 5 | 55 | 55 | | b | 1 | 1 | 2 | 8 | 89 | - | > 圖表引用自 [2022 年 Linux 核心設計/實作課程作業 —— fibdrv - HackMD](https://hackmd.io/@sysprog/linux2022-fibdrv#-%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97) 引用 Calculating Fibonacci Numbers by Fast Doubling 文章底部的實作 ```c long long fibseq_basic_fast_doubling_branch(unsigned int offset) { unsigned long long h = 0; for (int i = offset; i; i >>= 1, h++) ; unsigned long long a = 0, b = 1; // fib(0), fib(1) for (unsigned long long mask = 1 << (h - 1); mask; mask >>= 1) { unsigned long long c = a * (2 * b - a); unsigned long long d = a * a + b * b; if (mask & offset) { a = d; b = c + d; } else { a = c; b = d; } } return a; } ``` 使用 ```__builtin_clz``` 的改進實作 ```c long long fibseq_basic_fast_doubling_branch(unsigned int offset) { unsigned long long mask = ULLONG_MAX ^ (ULLONG_MAX >> 1); mask >>= __builtin_clz(offset); 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; if (mask & offset) { a = d; b = c + d; } else { a = c; b = d; } } return a; } ``` 其中 ```unsigned long long mask = ULLONG_MAX ^ (ULLONG_MAX >> 1);``` 為將最高位元設為 ```1``` 使用 bitwise 的技巧,再改寫成以下程式碼 ```c long long fibseq_basic_fast_doubling_branchless(unsigned int offset) { unsigned long long mask = ULLONG_MAX ^ (ULLONG_MAX >> 1); mask >>= __builtin_clz(offset); 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 & offset); int bset = !!(mask & offset); a = d ^ ((c ^ d) & -aset); b = d + (c & -bset); } return a; } ``` ### 在 user space 中測試程式碼 考量上述的 fast doubling 手法,分為是否使用 bitwise 技巧的實作。在 user space 測試是否有差異。 新增 ```fibseq.h, fibseq.c```,用於實作上述使用 branch 與 branchless 的程式碼、新增 ```uclient.c``` ,實作如下: ```c #include <stdio.h> #include <time.h> #include "fibseq.h" #define NANOSECOND(x) ((x).tv_sec * 1e9 + (x).tv_nsec) int main() { for (int i = 0; i < 1000; i++) { struct timespec t1, t2, t3; clock_gettime(CLOCK_MONOTONIC, &t1); long long ret1 = fibseq_basic_fast_doubling_branch(i); clock_gettime(CLOCK_MONOTONIC, &t2); long long ret2 = fibseq_basic_fast_doubling_branchless(i); clock_gettime(CLOCK_MONOTONIC, &t3); long long ut = (long long) (NANOSECOND(t2) - NANOSECOND(t1)); long long ut2 = (long long) (NANOSECOND(t3) - NANOSECOND(t2)); printf("%d %lld %lld %d\n", i, ut, ut2, ret1 == ret2); } return 0; } ``` 在 MakeFile 中新增 ```make uclient``` 指令 ``` uclient: uclient.c fibseq.c $(CC) -o $@ $^ ``` 新增 gnuplot 檔案 ```scripts/uclient_plot.gp``` > 參考自 [KYG-yaya573142/fibdrv/plot.gp](https://github.com/KYG-yaya573142/fibdrv/blob/master/scripts/plot.gp) ``` reset set xlabel 'F(n)' set ylabel 'time(ns)' set title 'Fibonacci runtime in userspace' set term png set output "uclient_picture.png" set grid plot [0:92][0:] \ "uclient_time" using 1:2 with linespoints linewidth 2 pointtype 7 pointsize 0.5 title "fast doubling", \ "" using 1:3 with linespoints linewidth 2 pointtype 7 pointsize 0.5 title "bitwise hack", ``` 將以上流程寫成寫入 MakeFile 中,新增 ```make ucheck```: ``` ucheck: uclient ./uclient > ./uclient_time gnuplot scripts/uclieng_plot.gp ``` 最終產生以下圖片,可見在 userspace 中使用 bitwise 技巧比原本的實作多耗時 ![](https://i.imgur.com/OFL1KlT.png) <!-- 參考該範例,去思考此圖的消息 https://hackmd.io/BZjwYw1FQ1O0NPY5kIS7VQ?view#%E5%BF%83%E5%BE%97---%E6%B3%A8%E6%84%8F%E5%AF%A6%E9%A9%97%E8%A8%AD%E7%BD%AE --> 透過 ```gcc -S fibseq.c``` 輸出 x86_64 的組合語言程式碼,可參考[線上網站 Godbolt Complier Explorer](https://godbolt.org/) [fibseq_basic_fast_doubling_branchless](https://godbolt.org/z/Kz9j4fb78) ``` # int aset = !(mask & offset); movl -52(%rbp), %eax andq -8(%rbp), %rax testq %rax, %rax sete %al movzbl %al, %eax movl %eax, -44(%rbp) # int bset = !!(mask & offset); movl -52(%rbp), %eax andq -8(%rbp), %rax testq %rax, %rax setne %al movzbl %al, %eax movl %eax, -48(%rbp) # a = d ^ ((c ^ d) & -aset); movq -32(%rbp), %rax xorq -40(%rbp), %rax movq %rax, %rdx movl -44(%rbp), %eax negl %eax cltq andq %rdx, %rax xorq -40(%rbp), %rax movq %rax, -16(%rbp) # b = d + (c & -bset); movl -48(%rbp), %eax negl %eax cltq andq -32(%rbp), %rax movq %rax, %rdx movq -40(%rbp), %rax addq %rdx, %rax movq %rax, -24(%rbp) ``` [fibseq_basic_fast_doubling_branch](https://godbolt.org/z/jnqxnGGf4) ``` # if (mask & offset){ # a = d; # b = c + d; # } movl -52(%rbp), %eax andq -8(%rbp), %rax testq %rax, %rax je .L3 movq -40(%rbp), %rax movq %rax, -16(%rbp) movq -32(%rbp), %rdx movq -40(%rbp), %rax addq %rdx, %rax movq %rax, -24(%rbp) jmp .L4 # else { # a = c; # b = d; # } movq -32(%rbp), %rax movq %rax, -16(%rbp) movq -40(%rbp), %rax movq %rax, -24(%rbp) ``` > 目前初步推論使用 bitwise 技巧產生較多的組合語言程式碼跟更多的計算,導致效能比單純使用 branch 指令來得費時。 > [color=#ff0000] ## 在 user space 實作大數 > 以下程式碼實作在分支 [decnum](https://github.com/a1091150/fibdrv/tree/decnum),為第二個版本。 > 第一版本 [744e95c3a987a3b5822cd887234d9a667c5d9591](https://github.com/a1091150/fibdrv/tree/744e95c3a987a3b5822cd887234d9a667c5d9591) > 第二版本 [decnum head](https://github.com/a1091150/fibdrv/tree/decnum) 將 ```malloc``` 從 ```decnum_t``` 函式移除,改進記憶體使用方式 > 為了測試程式碼,先實作於 user space。 ### 新增的程式碼 #### 新增 ```decnum.c decnum.h``` 新增兩個檔案,用於實作大數 ```struct decnum``` 結構體 #### 新增 ```dclient.c``` ```c #define PRINTDECNUM(a) \ printf("%" PRIu32, (a).digits[((a).size - 1)]); \ for (size_t i = 1; i < (a).size; i++) { \ printf(",%09" PRIu32 "", (a).digits[((a).size - i - 1)]); \ } \ printf("\n"); int main() { decnum_t fib = DECNUM_INIT(0, 0); for (size_t i = 0; i < 100; i++) { decnum_fast_doubling(i, &fib); printf("%lu ", i); PRINTDECNUM(fib); decnum_free(&fib); } return 0; } ``` 隨著大數 ```decnum``` 相關的加法、減法、乘法實作,在每次 git commit 中,皆有 ```dclient.c``` 為對應的程式碼測試。 #### 修改 ```Makefile``` 內容 ```diff + dclient: dclient.c decnum.c fibseq.c + $(CC) -o $@ $^ ``` 新增 ```dclient``` 用於測試 ```decnum``` 程式碼內容 #### 修改 ```fibseq.h fibseq.c``` 在 ```fibseq.h fibseq.c``` 中新增 ```void decnum_fast_doubling``` 函式 ### ```decnum.c decnum.h``` 程式碼內容 #### struct decnum 大數結構體 ```c struct decnum { uint32_t size; uint32_t cap; int32_t *digits; }; typedef struct decnum decnum_t; #define DECMAXVALUE 1000000000 // 10^9 #define DECNUM_INIT(a, b) \ { \ .size = (a), .cap = (b), .digits = NULL \ } ``` 在原本的作業敘述中,$F_{93}$ 之後的數字會因為 overflow 導致輸出錯誤,採用 ```struct decnum``` 結構體表示一個數字。 - ```size```:紀錄大數的最高位有效位數(非零位數) - ```cap```: 實際使用 ```malloc``` 的記憶體量 - ```digits```: 表示一個數字。使用 ```int32_t``` 紀錄數字,使用 $10^9$ 進位模式,因此每個位數最大能儲存 $999,999,999$,最小為 $0$ 例如以下程式碼,若以 ```decnum``` 要表示 $1,999,999,999$,```digits[0]``` 表示最低位數字,而 ```digits[1]``` 表示第二位數字 ```c decnum_t b1 = DECNUM_INIT(2, 2); decnum_new(&b1); b1.digits[0] = DECMAXVALUE - 1; b1.digits[1] = 1; ``` 參考 Fast Doubling 方法 $Fib(2k) = Fib(k)[2Fib(k+1) - Fib(k)]$ $Fib(2k + 1) = Fib(k+1)^2 + Fib(k)^2$ 要實作的函式功能為:加法、減法、乘法、平方 以下的實作,程式碼主要分為兩個部份 - 執行函式要求的運算 - 計算後的結果的位數,調整 ```size``` 與 ```cap``` #### decnum_add function 兩個大數加法 ```c= void decnum_add(decnum_t *b1, decnum_t *b2, decnum_t *result) { if (!result || !b1 || !b2) { return; } if (b1->size > result->cap || b2->size > result->cap) { return; } decnum_t a1 = DECNUM_INIT(b1->size, b1->cap); a1.digits = b1->digits; decnum_t a2 = DECNUM_INIT(b2->size, b2->cap); a2.digits = b2->digits; if (b2->size > b1->size) { decnum_swap(&a1, &a2); } if (b1 != result && b2 != result) { decnum_clean(result); } int32_t carry = 0; for (size_t i = 0; i < a2.size; i++) { int32_t digit = a1.digits[i] + a2.digits[i] + carry; carry = digit >= DECMAXVALUE; result->digits[i] = digit % DECMAXVALUE; } if (carry) { result->size = a2.size; for (size_t i = a2.size; (i < a1.size) && carry; i++) { int32_t digit = a1.digits[i] + 1; result->size += 1; carry = digit >= DECMAXVALUE; result->digits[i] = digit % DECMAXVALUE; } if ((result->size < result->cap) && carry) { result->digits[result->size] = 1; result->size += 1; } } else { result->size = a1.size; memcpy(result->digits + a2.size, a1.digits + a2.size, sizeof(int32_t) * (a1.size - a2.size)); } } ``` 因為加法的特性,可以將運算完成的數字直接指派給原本的變數,例如 ```decnum_add(a,b,a)``` 將結果重新指派回 ```a``` ,就不需要新的空間指派。 處理兩數加法運算,將重疊的位數相加。再透過變數 ```carry``` 處理剩餘的位數 #### decnum_sub 兩個大數減法 ```c= void decnum_sub(decnum_t *b1, decnum_t *b2, decnum_t *result) { if (!result || !b1 || !b2) { return; } if (b1->size > result->cap || !b1->size || !b2->size) { return; } decnum_t a1 = DECNUM_INIT(b1->size, b1->cap); a1.digits = b1->digits; decnum_t a2 = DECNUM_INIT(b2->size, b2->cap); a2.digits = b2->digits; if (b1 != result && b2 != result) { decnum_clean(result); } int32_t carry = 0; for (size_t i = 0; i < a2.size; i++) { int32_t digit = a1.digits[i] - a2.digits[i] - carry; carry = digit < 0; result->digits[i] = digit + (DECMAXVALUE & -carry); } for (size_t i = a2.size; i < a1.size; i++) { int32_t digit = a1.digits[i] - carry; carry = digit < 0; result->digits[i] = digit + (DECMAXVALUE & -carry); } result->size = a1.size; size_t i = a1.size - 1; while (i && !result->digits[i]) { result->size--; i--; } } ``` 在 fast doubling 的計算過程中,$Fib(2k) = Fib(k)[2Fib(k+1) - Fib(k)]$ 用到減法運算,並且 $2Fib(k+1) \geqq Fib(k)$,因此實作中只有考慮 ```decnum_t b1``` 大於 ```decnum_t b2``` 下的減法運算 同樣因為減法的特性,可以將運算完成的數字直接指派給原本的變數,例如 ```decnum_add(a,b,a)``` 將結果重新指派回 ```a``` ,就不需要新的空間指派。 #### decnum_mult function 大數乘法 ```c= void decnum_mult(decnum_t *b1, decnum_t *b2, decnum_t *result) { if (!result || !b1 || !b2) { return; } if ((b1->size + b2->size) > result->cap) { return; } decnum_clean(result); result->size = b1->size + b2->size; const int32_t size = b1->size + b2->size; for (size_t i = 0; i < b1->size; i++) { for (size_t j = 0; j < b2->size; j++) { int64_t carry = ((int64_t) b1->digits[i]) * ((int64_t) b2->digits[j]); size_t k = i + j; do { uint64_t vv = carry + result->digits[k]; result->digits[k] = vv % DECMAXVALUE; carry = vv / DECMAXVALUE; k++; } while ((k < size) && carry); } } size_t i = result->size - 1; while (i && !result->digits[i]) { result->size--; i--; } } ``` 在第 21 ~ 26 行中,因為乘法的特性,會向更高位數傳遞 ```carry```,因此不能重新指派數值給原有的變數。 #### decnum_multi_by_two function 兩倍乘法 ```c= void decnum_multi_by_two(decnum_t *b1) { if (!b1) { return; } for (size_t i = 0; i < b1->size; i++) { b1->digits[i] <<= 1; } int32_t carry = 0; for (size_t i = 0; i < b1->size; i++) { int32_t digit = b1->digits[i] + carry; b1->digits[i] = digit % DECMAXVALUE; carry = digit / DECMAXVALUE; } if (carry && (b1->size < b1->cap)) { b1->digits[b1->size] = carry; b1->size++; } } ``` 提供大數一個乘以 2 的實作 ### 使用大數搭配 Fast Doubling #### decnum_fast_doubling 函式 ```c= void decnum_fast_doubling(unsigned int offset, decnum_t *result) { if (!result) { return; } const float factor = 42.25; unsigned int sp = (((float) offset) / factor) + 2; decnum_t a = DECNUM_INIT(1, sp); decnum_new(&a); if (!a.digits) { goto fa; } decnum_t b = DECNUM_INIT(1, sp); decnum_new(&b); if (!b.digits) { goto fb; } decnum_t tmp = DECNUM_INIT(1, sp); decnum_new(&tmp); if (!tmp.digits) { goto ft; } decnum_t tmp2 = DECNUM_INIT(1, sp); decnum_new(&tmp2); if (!tmp2.digits) { goto ft2; } decnum_clean(&a); decnum_clean(&b); decnum_clean(&tmp); decnum_clean(&tmp2); a.size = 1; a.digits[0] = 0; b.size = 1; b.digits[0] = 1; unsigned long long mask = ULLONG_MAX ^ (ULLONG_MAX >> 1); mask >>= __builtin_clz(offset); for (; mask; mask >>= 1) { if (mask & offset) { // A = (a + b)^2 - 2ab = a^2 + b^2 decnum_add(&a, &b, &tmp); decnum_mult(&tmp, &tmp, &tmp2); decnum_mult(&a, &b, &tmp); decnum_multi_by_two(&tmp); decnum_sub(&tmp2, &tmp, &tmp); // B = (a + b)^2 - a^2 decnum_mult(&a, &a, &b); decnum_sub(&tmp2, &b, &b); decnum_assign(&a, &tmp); } else { // B = a^2 + b^2 decnum_mult(&a, &a, &tmp); decnum_mult(&b, &b, &tmp2); decnum_add(&tmp, &tmp2, &tmp2); // A = a * (2 * b - a) decnum_multi_by_two(&b); decnum_sub(&b, &a, &b); decnum_mult(&b, &a, &tmp); decnum_assign(&b, &tmp2); decnum_assign(&a, &tmp); } } decnum_free(&tmp); decnum_free(&tmp2); decnum_free(&b); decnum_swap(result, &a); return; ft2: decnum_free(&tmp); ft: decnum_free(&b); fb: decnum_free(&a); fa: return; } ``` **神秘的數字 42.25** 透過觀察 $fib(x)$在 ```decnum_t``` 實作,將輸出的 ```decnum_t size``` 除以 $x$ 會得出一組穩定的數字 ,例如 $x$ 取以下範圍 - $0 \backsim 44$:需要 1 個 ```int32_t``` - $45 \backsim 87$:需要 2 個 ```int32_t``` - $950 \backsim 992$:需要 23 個 ```int32_t```, 950 / 23 = 41.304、992 / 23 = 43.130 - $34842 \backsim 34884$:需要 810 個 ```int32_t```, 34842 / 810 = 43.066、1035 / 24 = 43.148 選出一個 magic number:$42.25$,將 $\lfloor x/42.3 \rfloor + 2$ 作為每次使用 ```malloc``` 的大小 第 52 ~ 77 行中,只能使用 ```a```, ```b```, ```tmp```, ```tmp2``` 四個變數去取得計算結果,並且盡量不使用乘法實作。 ### 確認輸出 使用 ```make dclient``` 確認輸出 ``` 0 0 1 1 2 1 3 2 4 3 5 5 6 8 ... 91 4,660046610,375530309 92 7,540113804,746346429 93 12,200160415,121876738 94 19,740274219,868223167 95 31,940434634,990099905 96 51,680708854,858323072 97 83,621143489,848422977 98 135,301852344,706746049 99 218,922995834,555169026 ``` 交叉比對後,列出到 $Fib(99)$ 皆為正確的數字。 ## 將大數程式碼移植成 kernel space 版本 > 以下程式碼實作於 [```kdecnum```](https://github.com/a1091150/fibdrv/tree/kdecnum) 分支 ### 移植的程式碼 #### 新增 ```kdecnum.h kdecnum.c``` 檔案 - 原本用於 user space 中的 ```decnum_t``` 加上前綴 ```k```,改名成 ```kdecnum_t``` - 將相關的函式名稱也加入前綴,例如 ```void decnum_new(decnum_t *ptr)``` 改成 ```void kdecnum_new(kdecnum_t *ptr)``` - ```malloc``` 改為 ```kmalloc```, ```free``` 改為 ```kfree```, ```realloc``` 改為 ```krealloc``` #### 修改 ```Makefile``` 檔案 ```diff - TARGET_MODULE := fibdrv + TARGET_MODULE := fibdrv_new obj-m := $(TARGET_MODULE).o - $(TARGET_MODULE)-objs := fibdrv.o + $(TARGET_MODULE)-objs := fibdrv.o kdecnum.o ... + eclient: eclient.c + $(CC) -o $@ $^ ``` - 修改 TARGET_MODULE 名稱,避免使用 ```make``` 指令時發生錯誤 - 使用 ```eclient.c``` 測試 fibonacci driver #### 修改 ```fibdrv.c``` - 原本實作於 ```fibseq.c``` 的 ```decnum_fib_fast_doubling``` 函式移植到 ```fibdrv.c``` 中 - 修改原本 ```fibdrv.c``` 中 ```ssize_t fib_read```,將 ```b1.digits``` 大數數值指派給 ```buf``` - ```#define MAX_LENGTH 92``` 改為 ```#define MAX_LENGTH 150``` ```c #define MAX_LENGTH 150 static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { kdecnum_t b1 = KDECNUM_INIT(0, 0); int res = decnum_fib_fast_doubling(*offset, &b1); size_t ss = min(size, b1.size * sizeof(int32_t)); access_ok(buf, ss); res = copy_to_user(buf, b1.digits, ss); kdecnum_free(&b1); return res; } ``` > 參考 [K04: fibdrv](https://hackmd.io/@sysprog/linux2022-fibdrv#%E9%97%9C%E6%96%BC-clientc) > 核心驅動裝置不能直接寫入該硬體,需透過 ```copy_to_user``` 方能將數值內容複製,使得 client 端接收。 剛開始撰寫程式碼時,我使用以下程式碼,導致 Ubuntu 系統螢幕顯示卡頓,即使是只有一行也是卡頓。意外的是系統沒有崩潰。 ```c buf[0] = b1.digits[0] & 0xFF; ``` #### 修改 ```eclient.c``` ```c #define BUFSIZE 8 int main() { long long sz; int32_t buf[BUFSIZE]; memset(buf, 0, sizeof(int32_t) * BUFSIZE); int fd = open(FIB_DEV, O_RDWR); if (fd < 0) { perror("Failed to open character device"); exit(1); } lseek(fd, 99, SEEK_SET); sz = read(fd, buf, BUFSIZE * sizeof(int32_t)); decnum_t fib = DECNUM_INIT(BUFSIZE, BUFSIZE); fib.digits = buf; PRINTDECNUM(fib); close(fd); return 0; } ``` 在實作上幾乎與原本的 ```client.c``` 相同 ## 時間成本計算 <!-- > 參考的實作為 [sysprog21/bignum](https://github.com/sysprog21/bignum) --> > 以下實作於 [```timemeasure```](https://github.com/a1091150/fibdrv/tree/timemeasure) 分支 ### 修改的程式碼 > 以下實作的版本為 [```timemeasure 60268ca7215f84a3f0a973d05f7fda03bab3f2f5```](https://github.com/a1091150/fibdrv/tree/60268ca7215f84a3f0a973d05f7fda03bab3f2f5) 將 ```fibdrv.c``` 的程式碼的回傳改為計算 ``` decnum_fib_fast_doubling``` 需要的時間,單位為 nano second ```diff static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { kdecnum_t b1 = KDECNUM_INIT(0, 0); + ktime_t kk = ktime_get(); int res = decnum_fib_fast_doubling(*offset, &b1); + kk = ktime_sub(ktime_get(), kk); + ssize_t dd = ktime_to_ns(kk); size_t ss = min(size, b1.size * sizeof(int32_t)); access_ok(buf, ss); res = copy_to_user(buf, b1.digits, ss); kdecnum_free(&b1); - return res; + return dd; } ``` 在尚未移除影響效能的因素下,多次使用指令 ```make eclient``` 計算 $Fib(99)$,可得到以下輸出 ``` 28713 28342 34695 30788 30795 ``` ### 移除影響效能分析的因素 <!-- > 參考 [2022 年 Linux 核心設計/實作課程作業 fibdrv Linux 效能分析的提示 - HackMD](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) --> #### 使用 taskset 指定 CPU 核心上執行程式(指定 19) > cpu19 為 Intel effective core,用途為省電,使用 Performance core 在執行時間上較短 > 以下實作為 [```timemeasure 719aa1ef02dc949dbd4c65979a2698ae726acdc9```](https://github.com/a1091150/fibdrv/tree/719aa1ef02dc949dbd4c65979a2698ae726acdc9) 在 ```Makefile``` 中新增測試流程 ```diff= eall: all eclient $(MAKE) unload $(MAKE) load - sudo ./eclient > ./eclient_time + sudo taskset -c 1 ./eclient > ./eclient_time gnuplot scripts/eclient_plot.gp ``` 在多次執行 ```make eall``` 後,無論是否使用第四行或是第五行程式碼所繪製的圖片,皆有週期性的跳動 ![](https://i.imgur.com/ZnIuX78.png) 或是遠高於平均、低於平均的數值 ![](https://i.imgur.com/rAuJhFu.png) #### 設定 intel turbo mode、 抑制 ASLR、區隔指定的 CPU > 參考 [2022 年 Linux 核心設計/實作課程作業 fibdrv Linux 效能分析的提示,保留特定的 CPU](https://hackmd.io/@sysprog/linux2022-fibdrv#%E9%99%90%E5%AE%9A-CPU-%E7%B5%A6%E7%89%B9%E5%AE%9A%E7%9A%84%E7%A8%8B%E5%BC%8F%E4%BD%BF%E7%94%A8) > 參考 [grub2 - How do I add a kernel boot parameter? - Ask Ubuntu](https://askubuntu.com/questions/19486/how-do-i-add-a-kernel-boot-parameter) > 參考 [What is the difference between GRUB_CMDLINE_LINUX and GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub](https://askubuntu.com/questions/575651/what-is-the-difference-between-grub-cmdline-linux-and-grub-cmdline-linux-default) 在 ```/etc/default/grub``` 檔案中加入 ```GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=19"```,並且 ```sudo update-grub``` 並修改 ```Makefile``` ``` performance: # cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor sudo sh scripts/performance.sh powersave: # cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor sudo sh scripts/powersave.sh noturbo: sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo" noaslr: sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" irqaffinity: sudo sh scripts/changeIRQAffinity.sh eall: all eclient noturbo noaslr performance irqaffinity $(MAKE) unload $(MAKE) load sudo taskset -c 19 ./eclient > ./eclient_time gnuplot scripts/eclient_plot.gp ``` 使用 ```make eall``` 後,產生以下圖片 ![](https://i.imgur.com/bEQAOp9.png) 好像什麼沒變化 ## 檢查實驗因素 在這次作業中,我使用 [@KYWeng 的筆記](https://hackmd.io/@KYWeng/rkGdultSU) 作為對照組去檢驗我的實作。忽略第 0 ~ 5 次的 $Fib(99)$ 計算,並顯示於以下圖片,此實驗獲得平穩的結果,表示確實移除影響效能分析的因素。 ![](https://i.imgur.com/lnbyfWV.png) ### ftrace and trace-cmd 使用 [```trace-cmd```](https://man7.org/linux/man-pages/man1/trace-cmd.1.html) 追蹤我的實作:使用 ```sudo trace-cmd record --max-graph-depth 1 -e all taskset -c 19 ./eclient > ./eclient_time```,並將結果輸出為下圖。執行時間驚人的增加 7 倍。 ![](https://i.imgur.com/K85dpmY.png) 使用 ```trace-cmd report``` 輸出的報告如下 ``` eclient-9916 [019] 1781.863070: kmalloc: (kdecnum_new+0x23) call_site=kdecnum_new ptr=0xffff8fe002e101a0 bytes_req=4 bytes_alloc=8 gfp_flags=GFP_KERNEL eclient-9916 [019] 1781.863070: kfree: (kdecnum_free+0x17) call_site=kdecnum_free ptr=0xffff8fe002e10118 eclient-9916 [019] 1781.863070: kfree: (kdecnum_free+0x17) call_site=kdecnum_free ptr=0xffff8fe002e10b10 ... ``` 然而同樣的使用 ftrace 追蹤 @KYWeng 的實作,卻是沒有變化(下圖) ![](https://i.imgur.com/LO7umRN.png) 在觀察 ```trace-cmd report``` 輸出的報告後,我注意到我的實作在每次加法、減法、乘法、都會使用 ```kmalloc``` 並且很快就 ```kfree```,因 ftrace 追蹤行為導致在執行時間大幅增加。 對比於 @KYWeng 的實作輸出的報告,出現 ```kmalloc```, ```kfree``` 的頻率不多,參照其程式碼的實作中,使用 ```krealloc``` 而不是多次使用 ```kmalloc``` ## 減少大數運算的時間成本 ### 第一次改進 這一次的修改包含 - 修改大數系統的加法、減法、乘法函式,可參考 [master b3960dd6260b28872978d53720d1c66c4139b01f](https://github.com/a1091150/fibdrv/tree/b3960dd6260b28872978d53720d1c66c4139b01f) - 修改記憶體使用方式,使用 magic number 42.25 配置指定大小的記憶體。在 Linux driver 版本中,不能使用浮點數,因此採用 42 作為計算數值。 改進之後產生的輸出圖片如下: ![](https://i.imgur.com/0SZOQnA.png) 成為穩定執行時間成本的線段 38000 ns 下降至 11500 ns ### 第二次改進(使用 perf) 參考 [@KYWeng perf](https://hackmd.io/@KYWeng/rkGdultSU#perf) 說明 ``` sudo perf record -g --call-graph dwarf ./eclient sudo perf report --stdio -g graph,0.5,caller ``` 取得以下結果(省略部份輸出),可以得出實作中乘法與 ```memset_erms``` 指令絕大多數執行時間 ``` 59.11% 0.00% eclient eclient [.] main | ---main __GI___read (inlined) entry_SYSCALL_64_after_hwframe do_syscall_64 __x64_sys_read ksys_read vfs_read fib_read | |--29.81%--kdecnum_mult | --29.30%--memset_erms ``` 使用 ```sudo perf stat -r 10 -e cycles,instructions,cache-misses,cache-references,branch-instructions,branch-misses ./eclient``` 取得以下輸出, ```cpu_core/cache-misses/``` 佔去 59.19% ``` Performance counter stats for './eclient' (10 runs): 205,8354 cpu_core/cycles/ ( +- 1.36% ) <not counted> cpu_atom/cycles/ (0.00%) 426,1798 cpu_core/instructions/ ( +- 0.11% ) <not counted> cpu_atom/instructions/ (0.00%) 2274 cpu_core/cache-misses/ ( +- 52.19% ) <not counted> cpu_atom/cache-misses/ (0.00%) 1,9000 cpu_core/cache-references/ ( +- 7.76% ) <not counted> cpu_atom/cache-references/ (0.00%) 81,7867 cpu_core/branch-instructions/ ( +- 0.12% ) <not counted> cpu_atom/branch-instructions/ (0.00%) 6047 cpu_core/branch-misses/ ( +- 7.44% ) <not counted> cpu_atom/branch-misses/ (0.00%) 0.0011872 +- 0.0000302 seconds time elapsed ( +- 2.54% ) Some events weren't counted. Try disabling the NMI watchdog: echo 0 > /proc/sys/kernel/nmi_watchdog perf stat ... echo 1 > /proc/sys/kernel/nmi_watchdog ```