# F04: kcalc :::info 主講人: [jserv](http://wiki.csie.ncku.edu.tw/User/jserv) / 課程討論區: [2019 年系統軟體課程](https://www.facebook.com/groups/system.software2019/) :mega: 返回「[Linux 核心設計](http://wiki.csie.ncku.edu.tw/linux/schedule)」課程進度表 ::: ==[作業說明直播錄影](https://youtu.be/jyflHdZMB2s)== ## 預期目標 * 重新學習數值系統:浮點運算和定點數 * 留意 Linux 核心程式撰寫的限制 * 撰寫可適用於使用者和核心層級的程式 * 自動測試機制 * 透過工具進行效能分析 ## Linux 核心的浮點數運算 Robert Love 在 [Linux Kernel Development](https://www.amazon.com/Linux-Kernel-Development-Robert-Love/dp/0672329468/) 一書論及浮點運算: > No (Easy) Use of Floating Point > When using floating-point instructions kernel normally catches a trap and then initiates the transition from integer to floating point mode. Unlike user-space, the kernel does not have the luxury of seamless support for floating point because it cannot easily trap itself. Using a floating point inside the kernel requires manually saving and restoring the floating point registers. Except in the rare cases, no floating-point operations are in the kernel. * 解說: [Use of floating point in the Linux kernel](https://stackoverflow.com/questions/13886338/use-of-floating-point-in-the-linux-kernel) Rusty Russell 在 [Unreliable Guide To Hacking The Linux Kernel](http://landley.net/kdocs/htmldocs/kernel-hacking.html) 則說: > The FPU context is not saved; even in user context the FPU state probably won't correspond with the current process: you would mess with some user process' FPU state. If you really want to do this, you would have to explicitly save/restore the full FPU state (and avoid context switches). It is generally a bad idea; use fixed point arithmetic first. ## 數學式計算器 [Reverse Polish Notation](https://en.wikipedia.org/wiki/Reverse_Polish_notation) - Reverse Polish notation (RPN) 中 operator 會放在所有 operand 後面 - 平常所使用的是 infix notation ,用括號 `( )` 表示順序 範例: - Infix notation ![](https://i.imgur.com/Iad0t5O.png) * 運算子標為藍色,數值標為黑色 * human friendly - Reverse Polish notation (RPN) ![](https://i.imgur.com/lrCnadG.png) * computer friendly - infix notation 轉 postfix notation - [Shunting-yard algorithm](https://en.wikipedia.org/wiki/Shunting-yard_algorithm) - RPN evaluated from left to right - 遇到數值 push 進 stack - 遇到運算子取出 stack 前兩個運算,再把結果 push 進 stack - 最後 stack 中剩下的那個數 (在 s[0]) 為答案 ``` a : s[0] = a 1 : s[1] = 1 2 : s[2] = 2 + : s[1] = s[1] + s[2] b : s[2] = b * : s[1] = s[1] * s[2] + : s[0] = s[0] + s[1] 3 : s[1] = 3 - : s[0] = s[0] - s[1] ``` MathEX 是成功大學師生開發的數學表示式求值工具,接受一組數學表達式,輸出這段數學表達式所對應的答案。 取得 `MathEX` 原始程式碼並測試: ```shell $ git clone https://github.com/sysprog21/kcalc $ cd kcalc/mathex $ make check ``` 預期可見以下測試輸出: ``` OK: six=6, seven=7, six*seven == 42.000000 OK: 小熊=6, 維尼=7, 小熊*維尼 == 42.000000 OK: τ=1.618, 3*τ == 4.854000 OK: $(τ, 1.618), 3*τ() == 4.854000 ... Execute build/test-bench... BENCH 5: 9.383917 ns/op (106M op/sec) BENCH 5+5+5+5+5+5+5+5+5+5: 137.161016 ns/op (7M op/sec) BENCH 5*5*5*5*5*5*5*5*5*5: 127.733946 ns/op (7M op/sec) BENCH 5,5,5,5,5,5,5,5,5,5: 102.152109 ns/op (9M op/sec) BENCH ((5+5)+(5+5))+((5+5)+(5+5))+(5+5): 126.007080 ns/op (7M op/sec) ... ``` `MathEX` 基本的使用方式: ```clike #include "expression.h" /* Custom function that returns the sum of its two arguments */ static float add(struct expr_func *f, vec_expr_t args, void *c) { float a = expr_eval(&vec_nth(&args, 0)); float b = expr_eval(&vec_nth(&args, 1)); return a + b; } static struct expr_func user_funcs[] = { {"add", add, 0}, {NULL, NULL, 0}, }; int main() { const char *s = "x = 40, add(2, x)"; struct expr_var_list vars = {0}; struct expr *e = expr_create(s, strlen(s), &vars, user_funcs); if (!e) { printf("Syntax error"); return 1; } printf("result: %f\n", expr_eval(e)); expr_destroy(e, &vars); return 0; } ``` 上面的程式碼為範例,利用 `expr_create()` 將字串 `x = 40, add(2, x)` 輸入,再使用 `expr_eval()` 來產生輸出結果 ### MathEx APIs 這個工具提供了 4 個 API: - `struct expr *expr_create(const char *s, size_t len, struct expr_var_list *vars, struct expr_func *funcs)`:將輸入的字串數學表達式編譯成 `struct expr` 型態,並且回傳 - `float expr_eval(struct expr *e)`:計算 `struct expr` 的結果並回傳 - `void expr_destroy(struct expr *e, struct expr_var_list *vars)`:清除記憶體,`vars` 可以為 NULL (不清除 `vars` 的記憶體) - `struct expr_var *expr_var(struct expr_var_list *vars, const char *s, size_t len)`:在給定的變數清單 `vars` 中回傳對應名稱 `s` 的變數,如果清單中不存在該變數則會產生一個並回傳 ### 應用情境 這類的數學表達式分析工具可以被應用於工程計算機類的軟體,如:[insect](https://github.com/sharkdp/insect) 是個支援物理單位的工程計算機,其中程式碼 [insect/src/Insect/Parser.purs](https://github.com/sharkdp/insect/blob/master/src/Insect/Parser.purs) 就是計算機的數學表達式的解析器 ## 解析 `MathEx` 程式碼 透過 `expression.h` 的 macro 以及 struct 可以找到用來表示 expression 的 struct: ```clike #define vec(T) \ struct { \ T *buf; \ int len; \ int cap; \ } typedef vec(struct expr) vec_expr_t; struct expr { int type; union { struct { float value; } num; struct { float *value; } var; struct { vec_expr_t args; } op; struct { struct expr_func *f; vec_expr_t args; void *context; } func; } param; }; ``` 從上面的定義可以看到 `struct expr` 中,`param.op.args` 裡面其實包著 `struct expr` 的 pointer,在後續的程式中可以發現這個 pointer 會指向 1 或 2 個 `struct expr`,因此得知 `struct expr` 是一個 tree 的結構 接著可以透過 `expression.c` 中的 `expr_eval()` 中得知如何計算 expression 結果: ```clike float expr_eval(struct expr *e) { float n; switch (e->type) { case OP_UNARY_MINUS: return -(expr_eval(&e->param.op.args.buf[0])); case OP_UNARY_LOGICAL_NOT: return !(expr_eval(&e->param.op.args.buf[0])); case OP_UNARY_BITWISE_NOT: return ~(to_int(expr_eval(&e->param.op.args.buf[0]))); case OP_POWER: return powf(expr_eval(&e->param.op.args.buf[0]), expr_eval(&e->param.op.args.buf[1])); case OP_MULTIPLY: return expr_eval(&e->param.op.args.buf[0]) * expr_eval(&e->param.op.args.buf[1]); case OP_DIVIDE: return expr_eval(&e->param.op.args.buf[0]) / expr_eval(&e->param.op.args.buf[1]); case OP_REMAINDER: return fmodf(expr_eval(&e->param.op.args.buf[0]), expr_eval(&e->param.op.args.buf[1])); } ... } ``` 從上面可以知道,它透過遞迴呼叫,計算目前節點的左子節點跟右子節點的結果,最後根據自己的 operator 將左子節點跟右子節點的結果作運算 ### 效能分析 利用 `test-bench.c` 來查看結果: ```shell BENCH 5: 5.650043 ns/op (176M op/sec) BENCH 5+5+5+5+5+5+5+5+5+5: 39.499998 ns/op (25M op/sec) BENCH 5*5*5*5*5*5*5*5*5*5: 38.609982 ns/op (25M op/sec) BENCH 5,5,5,5,5,5,5,5,5,5: 42.056084 ns/op (23M op/sec) BENCH ((5+5)+(5+5))+((5+5)+(5+5))+(5+5): 34.670115 ns/op (28M op/sec) BENCH x=5: 3.753185 ns/op (266M op/sec) BENCH x=5,x+x+x+x+x+x+x+x+x+x: 44.126987 ns/op (22M op/sec) BENCH x=5,((x+x)+(x+x))+((x+x)+(x+x))+(x+x): 43.509007 ns/op (22M op/sec) BENCH a=1,b=2,c=3,d=4,e=5,f=6,g=7,h=8,i=9,j=10: 59.928894 ns/op (16M op/sec) BENCH a=1,a=2,a=3,a=4,a=5,a=6,a=7,a=8,a=9,a=10: 58.668852 ns/op (17M op/sec) ``` ## 重新學習浮點運算和定點操作 * [浮點數運算和定點數操作](https://hackmd.io/s/H1SVhbETQ) * [Fixed-point arithmetic](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) * [fixed point arithmetic for RT-Linux](http://netwinder.osuosl.org/pub/netwinder/docs/nw/rt_fixed/doc/html/rt_fix.html) * [include/rt_fix.h](http://netwinder.osuosl.org/pub/netwinder/docs/nw/rt_fixed/include/rt_fix.h) * [src/rt_fix.c](http://netwinder.osuosl.org/pub/netwinder/docs/nw/rt_fixed/src/rt_fix.c) * Linux 核心的 CFS 排程器就用到 fixed-point * [linux/include/linux/sched.h](https://github.com/torvalds/linux/blob/master/include/linux/sched.h) ```clike /* * Integer metrics need fixed point arithmetic, e.g., sched/fair * has a few: load, load_avg, util_avg, freq, and capacity. * * We define a basic fixed point arithmetic range, and then formalize * all these metrics based on that basic range. */ ``` * [linux/kernel/sched/fair.c](https://github.com/torvalds/linux/blob/master/kernel/sched/fair.c) ```clike /* * The exact cpuload calculated at every tick would be: * * load' = (1 - 1/2^i) * load + (1/2^i) * cur_load * * If a CPU misses updates for n ticks (as it was idle) and update gets * called on the n+1-th tick when CPU may be busy, then we have: * * load_n = (1 - 1/2^i)^n * load_0 * load_n+1 = (1 - 1/2^i) * load_n + (1/2^i) * cur_load * * decay_load_missed() below does efficient calculation of * * load' = (1 - 1/2^i)^n * load * * Because x^(n+m) := x^n * x^m we can decompose any x^n in power-of-2 factors. * This allows us to precompute the above in said factors, thereby allowing the * reduction of an arbitrary n in O(log_2 n) steps. (See also * fixed_power_int()) * * The calculation is approximated on a 128 point scale. */ ``` * [Floating Point Numbers and printk()](http://www.kernelthread.com/publications/faq/335.html) ## 自我檢查清單 * 解釋浮點運算在 Linux 核心中為何需要特別對待,以及 context switch 的過程中,涉及到 FPU/SIMD context,該注意什麼? * 提示: 參照 [Lazy FP state restore](https://en.wikipedia.org/wiki/Lazy_FP_state_restore) 和上方參考資料 * 應該撰寫對應包含浮點運算的 Linux 核心模組,實際編譯和測試 * 在給定的 `calc.c` 檔案中,和 [fibdrv](https://hackmd.io/s/SJ2DKLZ8E) 一樣有 character device,但註冊用的 kernel API 不同 (`register_chrdev` vs. `alloc_chrdev_region`),能否解釋其差異和適用場合呢? * 在 `scripts/test.sh` 檔案中,有一道命令為 `sudo chmod 0666`,這個作用為何?對於我們測試有何幫助?能否對 [fibdrv](https://hackmd.io/s/SJ2DKLZ8E) 建立的 `/dev/fibonacci` device file 也套用類似修改呢?另外,請解釋 device file 在核心及使用者層級的功能 * 在 `calc.c` 檔案中,用到 `copy_to_user` 這個 kernel API,其作用為何?本例使用該 API 做了什麼事?若是資料量增長,是否會有效能的嚴重衝擊呢? * 找出至少 3 個 Linux 核心中使用定點數的案例,搭配程式碼解說 * 提示: 參照 [Linux Kernel Load Average 計算分析 ](http://brytonlee.github.io/blog/2014/05/07/linux-kernel-load-average-calc/), [What Every Computer Scientist Should Know About Floating-Point Arithmetic](https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html), [Load average explained](https://wiki.nix-pro.com/view/Load_average_explained) * 是否知曉 MathEx 如何解析給定字串,從而分離出變數和對應數學表示法呢? * 如何對 `MathEx` 進行效能分析?能否找出相似的 math expression 來分析比較呢? * 提示: 參照 [muparserSSE - A Math Expression Compiler](http://beltoforion.de/article.php?a=muparsersse) * 在 `MathEx` 原始程式碼的 `expression.[ch]` 裡頭 `vec` 相關的程式碼,主要做什麼事?有沒有發現類似 [list](https://hackmd.io/s/S12jCWKHN) 使用到的技巧呢? * 提示: 參照 `mathex/test-unit.c` 的測試項目 * 解釋 `MathEx` 一類的 math expression 在真實世界有哪些應用?甚至,是否在 Linux 核心就存在類似的程式碼? * 提示: 參照 [A thorough introduction to eBPF](https://lwn.net/Articles/740157/) * 如果要將使用者層級的 C 語言程式,搬到 Linux 核心作為核心模組 (LKM),該注意哪些議題呢?請舉例說明 * 提示: 注意 `__KERNEL__` 巨集的定義, [kmalloc](https://www.kernel.org/doc/htmldocs/kernel-api/API-kmalloc.html) 的使用, [vmalloc](https://www.kernel.org/doc/htmldocs/kernel-api/API-vmalloc.html) 的使用 (以及後兩者的差異) * [fixed point arithmetic for RT-Linux](http://netwinder.osuosl.org/pub/netwinder/docs/nw/rt_fixed/doc/html/rt_fix.html) 的運作原理為何?給定的程式碼已經存在超過 20 年,很多細節已有出入,可否嘗試移植到 Linux `v4.15+` 呢? ## 作業要求 * 回答上述「自我檢查清單」的所有問題,需要附上對應的參考資料和必要的程式碼,以第一手材料 (包含自己設計的實驗) 為佳 * 在 GitHub 上 fork [kcalc](https://github.com/sysprog21/kcalc),主要目標是將 `MathEx` 整合到 `calc.c` 中 (作為 LKM 的形式),過程中需要一併完成以下: * 將 `MathEx` 的浮點數運算換為 fixed point,應該先在使用者層級驗證,然後再搬到 Linux 核心中。可斟酌移除 `MathEx` 裡頭的功能,但需要充分解釋; * 量化 `MathEx` 在核心的執行時間,搭配 [fibdrv](https://hackmd.io/s/SJ2DKLZ8E) 撰寫的工具程式使用; * 設計 micro-benchmarking 實驗,用以驗證 `MathEx` 移植到 Linux 核心後的表現; * 嘗試解讀上述時間分佈,特別是隨著 Fibonacci 數列增長後,對於 Linux 核心的影響; * 改善 `MathEx` 的執行效率; * 請善用 [perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) 一類地效能分析工具; ## 繳交方式 編輯 [Homework2 作業區共筆](https://hackmd.io/s/rygjaEK8V),將你的觀察、上述要求的解說、應用場合探討,以及各式效能改善過程,善用 gnuplot 製圖,紀錄於新建立的共筆 ## 截止日期 Mar 19, 2019 (含) 中午之前 越早在 GitHub 上有動態、越早接受 code review,評分越高