# 2020q3 Homework6 (kcalc) contributed by <`joe-U16`> [Github](https://github.com/joe-U16/kcalc-fixed) ###### tags: `sysprog2020` ## :penguin: 作業要求 * 在 GitHub 上 fork [kcalc-fixed](https://github.com/sysprog21/kcalc-fixed),目標是擴充數值運算器的功能,例如新增 `sqrt`, `Sigma` ($\sum$), `Pi` ($\Pi$) 等等,可從 [math-expression-evaluator](https://github.com/bugwheels94/math-expression-evaluator) 的 "Supported symbols" 選出幾項來實作。過程中應一併完成以下: - [x] 擴充數值運算器的時候,應一併提供測試案例,且該儘量反映出有效的數值範圍 - [ ] 留意乘法和加法的 overflow、減法的 underflow 等處理機制,分析後予以強化 - [ ] 確保數值運算的 precision 和 accuracy,甚至要考慮 [Kahan summation algorithm](https://en.wikipedia.org/wiki/Kahan_summation_algorithm) $\to$ 你可以修改原有 fixed-point 的設計和實作方式 - [ ] 觀察原始程式碼的 `expression.[ch]` 裡頭 `vec` 相關的程式碼,提出對空間效率更好的實作。考慮到 `kcalc-fixed` 的使用情境往往是持續接受一系列字串輸入,應避免頻繁的記憶體配置和釋放 - [ ] 修正現有 `kcalc-fixed` 內部數值運算的錯誤,並留意到不合法的記憶體操作; - [ ] 指出原本 fixed-point 實作的限制,觀察 Linux 核心所用的 fixed-point 運算程式碼,提出針對 kcalc 的改進方案 - [x] 原有的測試程式不會檢驗輸出數值,應予以擴充,得以自動和預先輸入的期望數值進行比對; [TOC] ## 同學做的 - [ ] https://hackmd.io/@sysprog/2020-kcalc - [ ] https://hackmd.io/@lQBEPrYxRoOVV2MatUYusg/r1dvv-G38 - [ ] https://hackmd.io/@sammer1107/sysprog_hw6_kcalc - [x] https://hackmd.io/@YLowy/Sy9fENauv - [ ] https://hackmd.io/@RinHizakura/HyH6mtK_D - [x] https://hackmd.io/@Rsysz/kcalc - [ ] https://hackmd.io/@guaneec/sysprog-2020q3-kcalc ## 修正現有 kcalc-fixed 內部數值運算的錯誤,並留意到不合法的記憶體操作; Logic 的 `<`, `>`, `==`, `!=`, `>=`, `<=` 是有問題的 ``` Testing 1<2 ... 0.0000000037252903 Testing 2>1 ... 0.0000000037252903 Testing 1==1 ... 0.0000000037252903 Testing 1!=2 ... 0.0000000037252903 Testing 1>=1 ... 0.0000000037252903 Testing 1<=1 ... 0.0000000037252903 ``` #### `kcalc` 使用 32-bit 整數來表達單精度浮點數(即 `float`) **Fixed-point representation** kcalc introduces a fixed-point number representation for fractional values: one 32-bit size is divided into mantissa (28 bits), sign (1 bit) and exponent (3 bits). ``` MSB 31 4 3 2 1 0 LSB +----------------------------------+------+----------+ | mantissa | sign | exponent | +----------------------------------+------+----------+ ``` ```c= #define MASK(n) (((n) > 0) << 4) enum compare_type { LOWER = 1, EQUAL = 2, GREATER = 4 }; static int compare(uint64_t a, uint64_t b) { int flags = 0; if (a < b) flags |= LOWER; if (a > b) flags |= GREATER; if (a == b) flags |= EQUAL; return flags; } ``` #### Logic `!(not)` `expression.c` ```c=342 static uint64_t not(uint64_t a) { // return !a; return 1000; } ``` ``` 1.0000023283064370 Testing !0 ... 0.0000002328306437 Testing !1 ... ``` **改為 `return 0`:** ```c=342 static uint64_t not(uint64_t a) { // return !a; return 0; } ``` ``` Testing !0 ... 0.0000000000000000 Testing !1 ... 0.0000000000000000 ``` ## 擴充數值運算器的功能 ### 嘗試增加 user_func_add 首先註冊 user_func_add ```c= static struct expr_func user_funcs[] = { {"add", user_func_add, user_func_nop_cleanup, 0}, {NULL, NULL, NULL, 0}, }; ``` 實作 `user_func_add`: ```c= noinline uint64_t user_func_add(struct expr_func *f, vec_expr_t args, void *c) { uint64_t a = expr_eval(&args.buf[0]); uint64_t b = expr_eval(&args.buf[1]); return a + b; } ``` 測試結果: ``` Testing add(1,2) ... OK: 3.000000 == 3.000000 Testing add(3,4) ... OK: 7.000000 == 7.000000 Testing add(3.14, 2.11) ... OK: 5.250000 == 5.250000 ``` ### Sigma 實作 根據[`MathEX` 基本的使用方式:](https://hackmd.io/@sysprog/2020-kcalc#-數學式計算器) ```c= static struct expr_func user_funcs[] = { {"sigma", user_func_sigma, user_func_nop_cleanup, 0}, {"add", user_func_add, user_func_nop_cleanup, 0}, {NULL, NULL, NULL, 0}, }; ``` 實作 `user_func_sigma`: ```c= noinline uint64_t user_func_sigma(struct expr_func *f, vec_expr_t args, void *c) { int64_t lower = expr_eval(&args.buf[2]); int64_t upper = expr_eval(&args.buf[3]); /* return if bad function call */ if (args.len != 4) return NAN_INT; if (lower > upper) return NAN_INT; uint64_t sum = 0; int64_t *i = (int64_t *) args.buf[0].param.var.value; int max_iter = INT_MAX; for (*i = lower; *i <= upper && max_iter >= 0; *i += (1LL << 32), max_iter--) { uint64_t tmp = expr_eval(&args.buf[1]); if (tmp == NAN_INT || tmp == INF_INT) return tmp; uint64_t new_sum; if (__builtin_add_overflow(sum, tmp, &new_sum)) { pr_info("calc: sigma, overflow occur\n"); return INF_INT; } sum = new_sum; } if (max_iter <= 0) return NAN_INT; return sum; } ``` 使用方式: 需輸入 4 個 augument ,分別為 notation, expression, start, stopㄅ e.g. `sigma(n, n*3, 1, 10)` 等價於數學式 $\sum\limits_{n = 1}^{10}{n\times3}$ 測試結果: ``` Testing sigma(n, n*3, 1, 10) ... OK: 165.000000 == 165.000000 Testing sigma(n, n, 1, 10) ... OK: 55.000000 == 55.000000 Testing sigma(i, i**2, 1, 10) ... OK: 385.000000 == 385.000000 Complete ``` ### Sqrt 實作 使用 `@RinHizakura` 同學的[程式碼](https://hackmd.io/@RinHizakura/HyH6mtK_D#sqrt) ```c= noinline uint64_t user_func_sqrt(struct expr_func *f, vec_expr_t args, void *c) { int64_t ix0 = expr_eval(&vec_nth(&args, 0)); /* for 0 or NAN or INF, just return itself*/ if (ix0 == 0 || ix0 == NAN_INT || ix0 == INF_INT) return ix0; /* first, scale our number between 1 to 4 */ int lz = __builtin_clzll(ix0); /* for negative number */ if (lz == 0) return NAN_INT; /* for range from 0 to 30 */ if (lz <= 30) { if (lz & 1) lz++; ix0 >>= (30 - lz); } /* for range from 31 to 63 */ else { if (!(lz & 1)) lz++; ix0 <<= (lz - 31); } int64_t s0, q, t, r; /* generate sqrt(x) bit by bit */ q = s0 = 0; /* [q] = sqrt(x) */ r = 0x400000000; /* r = moving bit from right to left */ while (r != 0) { t = s0 + r; // t = s_i + 2^(-(i+1)) if (t <= ix0) { // t <= y_i ? s0 = t + r; // s_{i+1} = s_i + 2^(-i) ix0 -= t; // y_{i+1} = yi - t q += r; // q_{i+1} = q_{i}+ 2^(-i-1) } ix0 += ix0; r >>= 1; } if (lz < 31) return (q >> 1) << ((30 - lz) >> 1); return (q >> 1) >> ((lz - 31) >> 1); } ``` 測試結果: ``` Testing sqrt(10) ... OK: 3.162278 == 3.162278 Testing sqrt(20) ... OK: 4.472136 == 4.472136 Testing sqrt(30) ... OK: 5.477226 == 5.477226 Testing sqrt(40) ... OK: 6.324555 == 6.324555 Testing sqrt(50) ... OK: 7.071068 == 7.071068 ``` 超過範圍或是不合法的輸入,也可以判斷出來。 ``` Testing INT_MAX=2147483647, sigma(n, n, 1, INT_MAX) ... OK: INF_INT == INF_INT Testing sigma(m, n, 2, 1) ... OK: NAN_INT == NAN_INT ``` #### 有效的數字範圍 **整數:** `INT32` 可表示的上限 `INT_MAX=2147483647` ```bash= test_op 'INT_MAX=2147483647, sqrt(INT_MAX)' '46340.950001' test_op 'INT_MAX=2147483647, sqrt(INT_MAX+1)' '46340.950012' ``` 超過上限整數上限會有問題 ``` Testing INT_MAX=2147483647, sqrt(INT_MAX) ... OK: 46340.950001 == 46340.950001 Testing INT_MAX=2147483647, sqrt(INT_MAX+1) ... FAIL: 0.000000 != 46340.950012 ``` **精準度瑕疵** 觀察至小數點後 16 位 * scripts/test.sh: ```bash= test_op 'sqrt(2)' ``` ``` Testing sqrt(2) ... 1.4142135622478587 ``` * sqrt() function in C Library `math.h` ```c= double b = sqrt(2); ``` ``` >b 1.4142135623730951 ``` 觀察兩者: 1.414213562==2478587== # scripts/test.sh 1.414213562==3730951== # math.h 可以發現到精度不足的問題。 ## 新增功能,可以自動和預先輸入的期望數值進行比對; ```c= ➜ kcalc-fixed git:(master) scripts/test.sh [sudo] password for papd: filename: /home/papd/kcalc-fixed/calc.ko version: 0.1 description: Math expression evaluation author: National Cheng Kung University, Taiwan license: Dual MIT/GPL srcversion: 1F24815BA4159D060435AAD depends: retpoline: Y name: calc vermagic: 5.4.0-58-generic SMP mod_unload Testing x=5, x = x+1 ... FAIL: 6.000000 != 1.000000 Testing six=6, seven=7, six*seven ... OK: 42.000000 == 42.000000 Testing 小熊=6, 維尼=7, 小熊*維尼 ... OK: 42.000000 == 42.000000 Testing τ=1.618, 3*τ ... FAIL: 4.854000 != 4.854000 Testing $(τ, 1.618), 3*τ() ... FAIL: 4.854000 != 4.854000 Testing nop() ... �4h� Complete ``` ### 功能錯誤 ``` Testing τ=1.618, 3*τ ... FAIL: 4.854000 != 4.854000 Testing 111.2 ... FAIL: 111.200000 != 111.200000 ``` 明明 `printf` 出來一樣,卻判斷為 `FAIL`。 再測試看看,印到更後面: ```c= printf("k_result=%.16lf, ans = %.16f\n\n", k_result, ans); ``` 觀察到兩者實際是不一樣的 ``` inte=4, frac=-627065226, d_frac = -0.146000 result=3.8539999997834675, ans = 4.8540000000000001 inte=123, frac=528280976, d_frac = 0.123000 result=123.1229999997008093, ans = 123.1230000000000047 ``` 看了這篇[Back to Basic: 談浮點數的比較](https://novus.pixnet.net/blog/post/30654542)後,才知道使用 `==` 來判斷 `kcalc` 的計算結果和手動輸入的兩個浮點數,是很愚蠢的一件事。 可以改成讓兩束差在可接受的範圍內就好。 可以計算絕對誤差: ```c= if (fabs(a - b) <= 0.001) ``` 但是離 0 越遠,接鄰兩浮點數之間的差就越大。所有可正常表示的 float 當中,只有在 [-1, 1] 區間內,接鄰兩數差才會小於 FLT_EPSILON;若在 [1, 2] 區間,接鄰兩數差恰好為 FLT_EPSILON,負數亦然。隨著數字增加,FLT_EPSILON 很快就會小到不像話,到了 16.0f 時,接鄰兩數差將達到 FLT_EPSILON 的 16 倍;到了16777216.0f 時,接鄰兩浮點數差已經大於 1.0。 這就是為什麼在 [-1, 1] 區間外使用 DBL_EPSILON、FLT_EPSILON 並不合理,就如同死守公分做為誤差單位,對於精密機械而言大得誇張,但對於計算交通路程來說卻又小得可笑,還不如實際考慮需求而設定誤差門檻。 在數值範圍變化大的場合,更好的作法是隨著尺度調整誤差門檻,也就是運用相對誤差。其中一個簡單的想法概念碼如下: ```c= int AlmostEqual(float a, float b, float tolerance) { float absA = fabsf(a); float absB = fabsf(b); return fabsf(a - b) <= tolerance * fmaxf(absA, absB); } ``` ### 目前實作: ```c= if (fabs(result - test) > 0.01) ``` 在數值太大時會出錯: ``` Testing 500000000000.4 ... FAIL: 1783793664.400000 != 500000000000.400024 ```