# 2017q1 team4 MathEX
constributed by <`henry0929016816`, `vtim9907`, `twzjwang`, `rayleigh0407`>
Task: 擴充 [MathEX](https://github.com/jserv/MathEX),設計高效能的數學表示式運算器,參考 [muparserSSE - A Math Expression Compiler](http://beltoforion.de/article.php?a=muparsersse&p=implementation)
# [muparserSSE - A Math Expression Compiler](http://beltoforion.de/article.php?a=muparsersse&p=implementation)
- Implementation Notes
- 寫 assembly code 效能很難超越使用所有優化的(現代的)compiler。(原因:由於使用者可能不太了解 intel 內部構造,無法依照硬體性能寫出好的 assembly code)
- 當你寫一個任何類型的 interpreter ,如果想用 just in time compiler 來提高性能,那麼最有可能需要 assembly language。
- compiled code 仍然比高度優化的interpreted code 快上許多。
- Streaming SIMD Extension 提供 8 個可以進行快速浮點數運算的暫存器,讓 CPU 實現 SSE。目的是同時運算 4 個數。
- SSE 指令集中包含 ss 的指令只運算一個值,這些是 muparserSSE 主要使用的指令子集 (i.e. movss, divss, mulss, addss, subss, ...)。
- The reverse polish notation
- 了解 muParserSSE 前必須先知道甚麼是 [reverse polish notation](https://en.wikipedia.org/wiki/Reverse_Polish_notation) 。
- Reverse Polish notation (RPN) 中 operator 會放在所有 operand 後面。
- 平常所使用的是 infix notation ,可以加上括號 `( )` 表示順序
- A simple Expression
- 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]
```
- 使用 SSE 指令集
```
a : movss xmm0, a
1 : movss xmm1, 1
2 : movss xmm2, 2
+ : addss xmm1, xmm2
b : movss xmm2, b
* : mulss xmm1, xmm2
+ : addss xmm0, xmm1
3 : movss xmm1, 3
- : subss xmm0, xmm1
```
- movss : 將浮點數移到暫存器
- addss subss mulss : 將兩個暫存器的值做加法、減法、乘法運算,存回第一個暫存器
- 算完後結果在 xmm0
- 動畫 (assume a=1 and b=2)
![](http://beltoforion.de/en/muparsersse//images/expr0.gif)
- 但並沒有想像中簡單
- A slightly more complex Expression
- 以下是個比較複雜的例子
![](https://i.imgur.com/NYaTMmj.png)
![](https://i.imgur.com/2BDvmv1.png)
- 轉成 pseudo assembly
```
1 : movss xmm0, 1
2 : movss xmm1, 2
3 : movss xmm2, 3
4 : movss xmm3, 4
5 : movss xmm4, 5
6 : movss xmm5, 6
7 : movss xmm6, 7
8 : movss xmm7, 8
a : movss xmm8, a ; we don't have a register xmm8!
* : mulss xmm7, xmm8 ; we don't have a register xmm8!
+ : addss xmm6, xmm7
+ : addss xmm5, xmm6
+ : addss xmm4, xmm5
+ : addss xmm3, xmm4
+ : addss xmm2, xmm3
+ : addss xmm1, xmm2
+ : addss xmm0, xmm1
```
- 只有 8 個 SSE 暫存器,但上面卻使用了第 9 個 (xmm8)
- 沒有辦法將所有內容存在 SSE 暫存器中,必須找到另一個解決方案
- 最低的 6 個暫存器(xmm0,xmm1,...,xmm5)用於直接存儲值,如果一個值需要存儲在較高的位置,它將直接存儲在CPU stack 上,剩餘的兩個 SSE 暫存器(xmm6和xmm7)用於運算 cpu stack 上的值。
- 動畫
![](http://beltoforion.de/en/muparsersse//images/expr1.gif)
- This library supports only callbacks with the cdecl calling convention.
- Arguments are pushed to the stack from right to left and the calling function has to clean up the stack afterwards
- Benchmarks
- muparserSSE 主要的功能是提昇運算的效能,但要兼顧運算的效能和準確性並不容易。
- 對 muparserSSE 來說,若算式裡不包含函式 (e.g. tan(), sin()),效能明顯好很多;如果算式裡有函式,效能可能會變原來的 1/2 ~ 1/5 倍。
- 有用進 Benchmark 裡的 parsers:
- Interpreter
- [fparser](http://warp.povusers.org/FunctionParser/) - Function Parser for C++ v4.2
- [MTParser](http://www.codeproject.com/KB/recipes/MathieuMathParser.aspx) - An extensible math expression parser with plug-ins
- [muParser](http://beltoforion.de/article.php?a=muparser) - a fast math parser library
- Compiler
- [MathPresso](https://code.google.com/archive/p/mathpresso/) - A Math expression parser from the author of asmjit
- muparserSSE - A math expression compiler
- 算式裡不包含函式的效能比較 ([測資](http://beltoforion.de/en/muparserx/bench_expr_without_functions.txt))
![](https://i.imgur.com/BnWI0OS.png)
- 算式裡包含函式的效能比較 ([測資](http://beltoforion.de/en/muparserx/bench_expr_with_fun.txt))
![](https://i.imgur.com/xfrGrfi.png)
# 原始版本
- `"x=5, y = 3, x+y"`
![](https://i.imgur.com/XtvQJAm.png)
> 一開始 expr_eval 的參數 struct expr e 的 type 是 OP_COMMA
> 分別以 e 的兩個 args.buf(type 為 `=`), args.buf(type 為 `,`)為參數
> 遞迴呼叫 expr_eval
# 實驗一 - 參考 muparserSSE 使用 SSE 指令運算
- 將 expr_create 建好的結果改用 SSE 運算
```C=
void expr_eval_sse_create(struct expr *e)
{
if (e->param.op.args.buf[0].type == OP_CONST)
top = eval_sse_stack_push(top, e->param.op.args.buf[0].param.num.value);
else
expr_eval_sse(&e->param.op.args.buf[0]);
if (e->param.op.args.buf[1].type == OP_CONST)
top = eval_sse_stack_push(top, e->param.op.args.buf[1].param.num.value);
else
expr_eval_sse(&e->param.op.args.buf[1]);
__m128 m1, m2, m3;
float f2 = eval_sse_stack_pop(top);
float f1 = eval_sse_stack_pop(top);
float f3;
m2 = _mm_load_ss(&f2);
m1 = _mm_load_ss(&f1);
switch (e->type) {
case OP_MULTIPLY:
m3 = _mm_mul_ss(m1, m2);
break;
case OP_DIVIDE:
m3 = _mm_div_ss(m1, m2);
break;
case OP_PLUS:
m3 = _mm_add_ss(m1, m2);
break;
case OP_MINUS:
m3 = _mm_sub_ss(m1, m2);
break;
}
_mm_store_ss(&f3, m3);
top = eval_sse_stack_push(top, f3);
}
float expr_eval_sse(struct expr *e)
{
top = NULL;
expr_eval_sse_create(e);
return eval_sse_stack_pop(top);
}
```
> 以上程式碼只能處理 + - * /
> 不能處理變數
- 結果
- 修改前
```=
BENCH 5+5+5+5+5+5+5+5+5+5: 43.447018 ns/op (23M op/sec)
BENCH 5*5*5*5*5*5*5*5*5*5: 38.432121 ns/op (26M op/sec)
BENCH ((5+5)+(5+5))+((5+5)+(5+5))+(5+5): 38.277864 ns/op (26M op/sec)
```
- 修改後
```=
BENCH 5+5+5+5+5+5+5+5+5+5: 442.471027 ns/op (2M op/sec)
BENCH 5*5*5*5*5*5*5*5*5*5: 433.096886 ns/op (2M op/sec)
BENCH ((5+5)+(5+5))+((5+5)+(5+5))+(5+5): 438.788891 ns/op (2M op/sec)
```
- 比較OP_CONST
- 慢了10倍以上...
BENCH|BENCH1|BENCH2|BENCH3
-|-|-|-
修改前(ns/op)|43.447018|38.432121|38.277864
修改後(ns/op)|442.471027|433.096886|438.788891
修改前/修改後|0.098|0.089|0.087
- 問題
- 過程中仍有遞迴呼叫
- stack 並非 [muparserSSE - A Math Expression Compiler](http://beltoforion.de/article.php?a=muparsersse&p=implementation) 中使用的 CPU stack
- 不能處理變數
> 原來的實做是把字串分析完之後 (infix notation -> postfix notation),用 tail recursion 快速算完。
> 如果要更快的話,可能要一邊分析字串的時候就要即時一邊算結果,比如針對分析的輸出,如果是數字就存進 stack,如果是符號,就直接 pop stack 做運算,應該比較符合 mupaserSSE 原來的設計。
> 我還在找怎麼改原來的 code 比較好 @@@ [name=偉庭]
>
# 使用 sse 的 register
* 由於 sse 的 register 有固定的名稱(xmm0,xmm1) ,所以為了動態的將資料傳入這些 register 我們可以使用 define ## 這個用法
`#define sse_register(x) xmm##x`
* 修改 vec_push
概念:
```c=
int register_ct=0
switch(e->type)
{
case OP_CONST:
movss sse_register(register_ct++) , val
break;
case OP_PLUS:
register_ct-=2;
addss sse_register(register_ct) , sse_register(register_ct++);
break;
}
```
# inline assembly code in C
為了做到讀取到的值,直接放入指定的 register 我們需要使用 inline assembly code 。
* ### GCC inline assembly code 語法
assembly 語言有兩種呈現的方式,一種是 intel Style ,另一種是 AT&T style 而 GCC 使用的是 AT&T 語法,所以接下來介紹的都是 AT&T 語法
* #### register 的命名方式:
register 的名稱都是由 ==%== 這個字符當作開頭,例如:`%eax ,%cl`
* #### 運算元的排序方式 :
不像 intel Style (第一個運算元是 destination), AT&T 則是將第一個運算元當作是 source , 第二個運算元當作是 destination ,所以照 intel Style syntax 寫這段 code `mov eax, edx` 在 AT&T syntax 就會變成 `mov eax, edx`
* #### 運算元的大小:
運算元的大小取決於 op-code 的結尾字符, b (byte) 為 8 bit , w(word) 為 16 bit , l(long) 為 32 bit , 對於以上的 code 正確的寫法應該是 ` movl %eax, %edx`
* #### 立即的運算元:
立即的運算元會使用 ==$== 當作開頭,例如:`movl $5, $eax`,就是將 long value 5 立即的傳到 register(%eax) 裡
* #### memory operand:
如果運算元沒有前綴字符,代表這個運算元為記憶體位址,因此 ` movl $bar, %eax` 這段 code 是要將 bar 這個變數的 address 傳到 %eax ,但是 `movl bar, %eax` 則是要將 bar 這個變數的內容傳到 %eax 這個 register
* #### indexing:
`movl, 8(%ebx),%eax` 這段 code 是將 %ebx 的位址往後移 8 位,將這個位址的內容傳到 %eax 這個 register
* ### Basic inline Code
我們可以使用以下兩種方式來撰寫我們的 assembly code:
* `asm("assembly code");`
* `__asm__ ("assembly code");`
# 改善
## Evaluate vs Execute
- Evaluate
- expression
- 求值
- Execute
- instructions
- 執行
## 為啥會變快,關鍵
- Cache friendly
- 以 cache locality 分析
## Psuedo assembly 跟可執行的 assembly 差在哪
## 不同表示式差異
## 為啥要定義先乘除後加減
## 可不可以有括號,演算法
## 正確性分析
## 缺 evaluator
## 一開始會變慢的問題在 parser
# Reference
[Unary Operation](https://en.wikipedia.org/wiki/Unary_operation)
[Using Inline Assembly in C/C++](https://www.codeproject.com/Articles/15971/Using-Inline-Assembly-in-C-C)
###### tags: `twzjwang`