# 2020q1 Homework4 (kcalc)
contributed by < `eecheng87` >
###### tags: `linux2020`
# 自我檢查清單
## 找出至少 2 個 Linux 核心中使用定點數的案例,搭配程式碼解說
### Linux Kernel Load Average 計算分析
事實上,作業說明提供的連結已經把相關的程式碼都分類好了,所以可以很直接的看到 Linux 核心如何使用定點數。以下統整連結的內容和補充沒提到的細節。
首先,Load Average 顧名思義就是要查看近期的負載平均,近期可分成三種時間間隔。而計算的公式如下:
$load_t = n \times \alpha + (1- \alpha) \times load_{t-1}$
這個公式其實很常見,在作業系統決定下一個 cpu burst 時就會套這個 [Exponential average](https://www.geeksforgeeks.org/shortest-job-first-cpu-scheduling-with-predicted-burst-time/) 公式。這個公式的好處是簡單且省記憶體的把過去的**歷史資料**都記住,而且會 aging。而很明顯的,這個公式的計算絕對牽涉的浮點數運算,而 kernel 是不能做浮點運算的,所以定點運算就派上用場了。
### Fixed Point Arithmetis for RT-Linux
[這是](http://netwinder.osuosl.org/pub/netwinder/docs/nw/rt_fixed/doc/html/rt_fix.html) linux 中另外一個定點運算的例子,和前者不一樣的是,這個專案實做了自己的 API,此外定義的定點數格式也和前者不同。詳細內容和 MathEx 實做的定點數將於文末討論。
## MathEx 如何解析給定字串,從而分離出變數和對應數學表示法呢?
先從 `test-simple.c` 下手,觀察如何運算出結果。
```cpp=19
int main()
{
const char *s = "x1 = 40, add(2, x1)";
struct expr_var_list vars = { 0 };
struct expr *e = expr_create(s, strlen(s), &vars, user_funcs);
if (!e) {
printf("Syntax error");
return 1;
}
float result = expr_eval(e);
```
我們可以觀察到,要透過 expression 得到結果須經過
* 將用 string 表示的 expression 轉成正規的格式 ( 透過 `expr_var_list` )
* 由上個步驟得到的 list 運算出結果
接著打開 `expression.c` 觀察 `expr_create` 的實做方式:
```cpp=502
for (;;) {
int n = expr_next_token(s, len, &flags);
```
首先把目前**剩下的**表示式傳入 `expr_next_token`,則 `n` 會得到下一個合理的 token 結尾的位置,透過 `n` 來取得 `char* token` 且記得維護新的**剩下的**表示式。( 關於 `expr_next_token` 是如何實做的,稍後會詳細說明 )
接著從 572 行開始就是判斷剛剛得到的 token 應該屬於那一個種類,並把不同類的 token 中的資料拿出來維護,以下說明面對不同類型 token 會做的事:
* 573 行的 else block 是處理遇到左括號 '(' 的區塊,左括號的添加又分成兩種情況
* 允須加:顧名思義,可有可無,交給使用者決定。
* 必須加:當 call function 後,預期緊接著的是左括號,e.g. `add(...`。
* 588 行的 else block 是處理遇到右括號 ')' 的區塊,簡單說明這個區塊該考慮什麼事:
* `while (vec_len(&os) > minlen ..`
* `if (str.n == 1 && *str.s == '{')` 當已經呼叫函數和讀完參數後,現在讀到 ')' 則進入。
* 686 行的 else block 是處理常數的區塊,並將數字包裝後 append 到 `es` 尾巴。
* 689 行的 else block 是處理 operand 的區塊,而定義的 operand 在 `expr_type` 中,只要不是 `OP_UNKNOWN` 一律進這個 block。但比較特別的地方是,逗號 comma 也有被定義在 `expr_type` 中。這個符號特別的地方在他有可能出現在用來區隔函數內的參數,若是這種情況,可以在 `for(;;;)` 的第一個 `if` 內看到,並且更新資料結構 `as`。
* 719 行的 else block 是處理**變數名稱**或**函數名稱**,在這個 block 只會紀錄 id 和 id 的長度。接著在下一個 iterate 的 543 行我們才會透過 `expr_var` 將這個新的變數包裝起來,並且 append 到 `es` 的尾端。若 id 是函數名稱,如:`add`。則會進 `if (n == 1 && *tok == '(') ..` ,內部會有 `expr_func(funcs, id, idn)` 檢查是否有這個 function name ( 有什麼 function name 是由 user 決定,透過傳入的參數 `funcs` ),有的話把 function name 放到 `os`,反之則報錯。
## 在 MathEx 中,nop 一類使用者自訂的函數如何實作呢?
當需要額外新增自定義函數時可以透過添加項目到 `struct expr_func user_funcs[]` 中,其中第一個成員要填這個函數的代稱,再來需要填實作的 function name。
舉例來說,若我想新增 fib,而 fib 的實作是 `int fibonacci(...)`,那應該這麼填 :
```cpp
static struct expr_func user_funcs[] = {
{"fib", user_fib, NULL, 0},
};
```
這個陣列在進入 `expr_create` 時會傳入,用來檢查表示式中是否有包含自定義的函數。在 905 行可以發現
```cpp=905
struct expr_func *f = expr_func(funcs, str.s, str.n);
```
將 `user_funcs` 傳入 `expr_func` 做檢查,檢查 `user_funcs` 中是否含有表示式中呼叫的函數。若有的話回傳那個 function 的資料。 `expr_func` 的實作如下:
```cpp
struct expr_func *expr_func(struct expr_func *funcs, const char *s, size_t len)
{
for (struct expr_func *f = funcs; f->name; f++) {
if (strlen(f->name) == len && strncmp(f->name, s, len) == 0)
return f;
}
return NULL;
}
```
可以清楚看到,是在做名字匹配的檢測。若找不到當然回傳 NULL。
## 是否發現 MathEx 對於負數的除法結果不正確、乘法和加法的 overflow 沒有做處理,且缺乏對減法的 underflow 處理機制呢?你預計如何克服?
參考 RT-Linux 實做乘法時,也有考慮到 overflow 和 underflow 的狀況。
```cpp
if(iwl > RT_FIXWL) {
if(rt_fix_sign(x) ^ rt_fix_sign(y)) {
x = rt_fix_min;
} else {
x = rt_fix_max;
}
rt_fix_errmergeset(&x, y);
rt_fix_erraddset(&x, RT_FIX_OV);
return x;
}
```
`iwl` 是類似乘數和被乘數相加的值,用這個值判斷是否超過 `RT_FIXWL`,即 31 bit。如果超過,就是溢位。再來透過 XOR 來檢查兩者的 sign bit ,藉此判斷是 overflow 或是 underflow。
## 整合 fibdrv 到 calc
目前專案 kcalc 的狀況是已經引入 MathEx 的 API,而 MathEx 提供使用者方便引入自定義函數,簡單範例可以考[這裡](https://github.com/jserv/MathEX/blob/master/test-simple.c)。而我們可以參照範例,把 fib 加入函式庫。
```cpp
static struct expr_func user_funcs[] = {
{"nop", user_func_nop, user_func_nop_cleanup, 0},
{"fib", user_fib, user_fib_cleanup, 0},
{NULL, NULL, NULL, 0},
};
noinline int user_fib(struct expr_func *f, vec_expr_t args, void *c){
printk("I am in user_fib\n");
return 100;
}
```
首先註冊函數基本資料,然後簡單寫個 fib 運算,這裡只是測試能夠正常擴充,所以尚未實做運算。預期能得到結果:
```shell
$ echo -ne "fib(5)" > /dev/calc
$ dmesg
[26595.020599] calc: Received 6 -> fib(5)
[26595.020618] I am in user_fib
[26595.020622] calc: Result: 100
[26595.020636] calc: Device successfully closed
```
以上,順利完成擴充。接著我們來學習如何運用 livepatch 覆蓋原本的 fib() 運算。打開 livepatch-cal.c ,簡單觀察之後發現,覆蓋原本模組函數的機制很簡單,只需要在 `klp_func funcs[]` 註冊新的運算,然後而外實做運算 ( 想要覆蓋的新演算法 )。了解需要改的地方之後,接著來驗證一下。
```cpp
{
.old_name = "user_fib",
.new_func = livepatch_fib,
},
```
接著記得把 livepatch 模組掛載上去,得到以下輸出:
```shell
[ 3207.492266] livepatch: 'livepatch_calc': patching complete
[ 3216.199506] calc: Received 5 -> fib()
[ 3216.199528] livepatch_calc: function fib is now patched
[ 3216.199540] calc: Result: 0
[ 3216.199555] calc: Device successfully closed
```
可以清楚看到 livepatch 內的函數成功取代原本的演算法。
## 觀察原始程式碼的 expression.[ch] 裡頭 vec 相關的程式碼,將 quiz4 使用到的技巧予以應用,提出對空間效率更好的實作
首先回顧 quiz4,在 folly 實做的 vector 有提到幾個改善效能的議題:
* 減少 malloc, realloc 的次數
* 如何選擇 realloc 的大小
### 減少 malloc, realloc 的次數
第一點,malloc 和 realloc 其實很花時間,像是 realloc 如果新要的記憶體無法以目前的記憶體做連續的延伸,那就得再找其他一塊連續的記憶體,接著更討厭的是還要把先前的那一堆東西複製到新的記憶體。所以可想而知,記憶體的配置策略不但會影響**空間的**效能,也會影響**速度的**效能。但是這兩種效能我認為無法並進,只能在當中取得平衡。所以我在本專案的策略並不一定是最好,只是在兩者間取得平衡。
首先,我希望達到的目標是,減少 alloc 的次數。事實上,這個目標不夠嚴謹,若要達到最少次數,那我第一次直接要一大塊就好了,這樣 push 時永遠不會超過 capacity。所以,我們要做的應該是**合理且有限度的**減少 alloc 的次數。接著我們就來觀察哪些地方可以減少配置的次數。
仔細觀察 `expression.h` 中的 `vec_init()`
```cpp
#define vec_init() \
{ NULL, 0, 0 }
```
當 vec_expr_t 類別的變數被創立的時候呼叫,預設 capacity 是 0。但是仔細想想,這樣的安排是不好的,因為在所有的運算中,宣告的 vec_expr_t 類別變數,如:`es`。有很大的機率會新增 vector 的成員,所以將會遇到無法避免的 alloc。不過再仔細觀察處理 create_expression 時 vector 大小的變化可以發現,有某幾 struct expr 型態的變數都會經歷由容量 0 擴增 ( realloc ) 成 1,再擴增成 2。 既然如此,不如把 vector 的初始大小改成 2,以減少呼叫 realloc 的次數。此外,每次 create_expression 會創造的 vector 數量其實不多,所以每個都預設初始大小為 2 並不會造成太大的負擔。
接著著手修改 expression,找出 vector_init 定義的巨集:
```cpp
#define vec_init() \
{ malloc(2 * 40), 0, 2}
```
這個巨集當初始化 `es`, `os` 等等會被呼叫。這裡為了方便,沒有把分配的大小做彈性化處理,直接給 40 ( 一個 vec_expr_t 的大小 )。
再來將原本 `struct expr binary = expr_init()` 改成:
```cpp
struct expr binary = { \
(enum expr_type)0, \
{ \
.op = vec_init() \
} \
};
```
目前已經把所有初始化的大小都改成我的版本了,使用 `MathEx` 內提供的 test-bench.c 來測試效能。測試的項目大概是以下格式,目的是為了看看兩種不同方案的 allocate policy 速度差異如何:
```shell
test_benchmark("5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
test_benchmark("5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5+5");
..
```
分別用原版本 ( original allocator ) 的和我修改的版本 ( my allocator ) 在 gnuplot 展示速度差異,以下是 expression 長度對速度作圖:
![](https://i.imgur.com/MLqmxG0.png)
可以發現若對初始化大小做修改的話,速度整體上來說能比原版本的略快。
### 如何選擇 realloc 的大小
第二點,在 folly 文件有提到,而我也有在 [quiz4](https://hackmd.io/W16BH33uRCyHup-Ob3sBPQ#%E8%A8%98%E6%86%B6%E9%AB%94%E9%85%8D%E7%BD%AE%E7%AD%96%E7%95%A5) 解釋為什麼要把擴大記憶體的 factor 設為 1.5。其實選擇 1.5 也有一個好處是,容易用 bitwise operation 實現。
接著來修改程式碼:將原本的
```cpp
int n = (*cap == 0) ? 1 : ((*cap << 1));
```
改成
```cpp
int n = (*cap == 0) ? 1 : ((*cap << 1) - (*cap >> 1));
```
再來設計一個簡單的實驗來看看效能如何,原本 MathEx 底下有提供 `test_benchmark` 來評估速度,邏輯是重複執行一百萬次,然後平均。雖然沒有拿掉 outliar ,但是為了方便還使勉強可以接受。現在要製造的情境是會一直 realloc,所以可以透過
```cpp
test_benchmark("a=1");
test_benchmark("a=1,a=1")
...
```
當然,為了方便,所以要寫一個 `char* repeat(char* orig, size_t times)` 來達到目的。這樣就能透過以下呼叫,來達到不同長度的執行。
```cpp
for(int i = 0; i < 3000; i++){
char *tmp = repeat("a=1,",i);
tmp[strlen(tmp) - 1] = '\0';
test_benchmark(tmp);
free(tmp);
}
```
![](https://i.imgur.com/snUd3MD.png)
可以看到,兩種 factor 的大小似乎對效能沒什麼提昇。選定 1.5 的原因是能夠重複利用之前已經 alloc 的區塊,而不用再複製一份一樣的到新的記憶體,進而增加速度。但是,我從上個作業就開始在懷疑改變 factor 真的能增加效能? 現代的 allocator 在 realloc 時真的會每次都要一塊全新的,然後再複製之前的到新記憶體,最後刪掉之前要的記憶體? 如果現代的 allocator 沒這麼笨的話,或許改變 factor 對效能起不了什麼太大的作用。
## 指出原本 fixed-point 實作的限制,觀察 Linux 核心所用的 fixed-point 運算程式碼,提出針對 kcalc 的改進方案
在 MathEx 中定點數的格式是:
```shell
28 | 1 | 3
----------------
int sign frac
```
而這裡的 frac 是指數的次方數,若是 -5,則最後的結果將是 $Integer \times 10^{frac}$。
而在 RT-Linux 中定點數的格式是:
```shell
1 | 14 | 17
-------------------
sign int frac
```
這邊轉換的方式我寫了一個函數,我認為會比較容易理解這種定點數是怎麼表示的
```cpp
void rt_fromfix(int tar){
double result;
result = (double)tar;
result = result / (1 << (31 - 14));
printf("%lf ",result);
}
```
### 兩種表示法之比較 ( MathEx v.s RT-Linux )
簡單列出幾個能比較的項目,分別是:
* 精準度
* 表示範圍
* 相關運算的速度
#### 精準度
在討論之前,先補上 MathEx 表示法如何轉換成浮點數的函數:
```cpp
void fromfix(int tar){
int num = tar >> 4;
int frac = tar & 15;
int neg = (frac & 8) >> 3;
if(neg > 0)
frac = -1 * ((~frac & 15) + 1);
printf("%lf\n",(double)num * pow(10, (double)frac));
}
```
在小範圍的精準度而言,可以觀察到 RT-Linux 限制在 0.000008 ( 約 $2^{-17}$ )。反觀,MathEx 的精準度可以高達 0.0000001 ($10^{-7}$),下面用分佈圖展示兩種表示法能表達的密度:
![](https://i.imgur.com/u8N9ZRY.png)
可以很直覺的看出,MathEx 精準度比較好 ( 事實上,**精準度兩者各有好和壞的時候** )。但是,當表示的數字很大的時候,MathEx 的即大缺陷將會顯現。若 frac > 0,精準度將降為 $10^0$ 以上。反觀,RT-Linux 的表示法下,不論何種情況,精準度都是 $2^{-17}$,因為都會除以 `(1 << (31 - 14))`。
#### 表示範圍
RT-Linux 能表示的最大值落在 16384 左右 ( $\frac{2^{31}}{2^{14}}$ ),相較 MathEx 的 $2^{28} \times 10^8$ 就顯得很小。不過當 MathEx 來到這麼大的值下,精準度也不會好到哪裡,都是 $10^3$ 起跳。
#### 相關運算的速度
這裡來分別測試 MathEx 和 RT-Linux 在定點數轉浮點數,定點數乘法的速度。
##### 定點數轉浮點數
兩種表示法轉換的程式碼在前面已經提及,分別是 `fromfix` 和 `rt_fromfix`,簡單設計一個實驗來看看速度表現:
```cpp
for(int i = 100; i < 1000000; i++){
clock_gettime(CLOCK_REALTIME, &t1);
rt_fromfix(i);
clock_gettime(CLOCK_REALTIME, &t2);
printf("%d ", i);
printf("%ld ", elapse(t1, t2));
clock_gettime(CLOCK_REALTIME, &t1);
fromfix(i);
clock_gettime(CLOCK_REALTIME, &t2);
printf("%ld \n", elapse(t1, t2));
}
```
![](https://i.imgur.com/QgkZwl8.png)
若放大來看會發現 MathEx 轉換的速度會比較快。
##### 定點數乘法的速度
MathEx 的乘法沿用 expression.c 內提供的函數。
而 RT-Linux 的乘法是用組合語言寫的,擷取片段解釋:
```cpp
rt_fix rt_fix_mul(rt_fix x, rt_fix y,int i)
{
unsigned short iwl = x._iwl + y._iwl;
..
if(y._iwl < iwl) {
y._value >>= x._iwl;
}
if(x._iwl < iwl) {
x._value >>= y._iwl;
}
x._iwl = iwl;
__asm__("imull %%ebx\n\t"
"shrdl %%cl, %%edx, %%eax\n\t"
: "=a" (x._value)
: "a" (x._value), "b" (y._value), "c" (RT_FIXWL - x._iwl)
: "edx");
rt_fix_min_iwl(&x);
return x;
}
```
省略的部份是檢查 overflow 或 underflow 的判斷。成員變數 `_iwl` 是整數的 bit 數,我們選 14。而 `_value` 表示定點數。`rt_fix_mul` 首先會把兩個數字對齊 ( 但我們傳入的都是 14,所以沒差 ),接著組合語言 `imull` 負責做乘法 ( $a \times b$ ),`shrdl` 負責 shift ( 因為定點數相乘最後需要右移修正,在 Linux Kernel Load Average 計算分析有說明相同的概念 )。
再來又是簡單寫個實驗來評估兩種乘法的速度:
```cpp
for(int i = 0; i < 1000; i++){
rt_fix a, b, c;
a._value = 61526515;
a._iwl = 14;
b._iwl = 14;
b._value = 54545454;
c._iwl = 14;
c = rt_fix_mul(a,b,i);
}
```
![](https://i.imgur.com/XAbxAqs.png)
結果有點令人小失望,用組合語言寫的乘法竟然沒有比較快。