--- tags: sysprog21-hw, 0140454, linachiu --- # 2016q3 Homework5 (jit-compiler) contributed by <`0140454`>, <`linachiu`> GitHub: [jit-construct](https://github.com/0140454/jit-construct) ## 作業要求 [A09: jit-compiler](https://hackmd.io/s/SJ7AZJv1e#作業要求) ## Brainfuck 程式語言 ### 打造 Brainfuck 的 JIT compiler Brainfuck 是種極為精簡的程式語言,由 Urban Müller 在 1993 年發展。Urban Müller 當初的目標為提出一種簡單的、可用最小的編譯器來實現、符合 Turing complete 的程式。 最早在 Amiga 機器上撰寫的編譯器只有 240 bytes 的大小,而 Brian Raiter 在 1999 年甚至於 i386/Linux 機器上做出僅需 166 bytes 的 Brainfuck 編譯器。 Brainfuck 僅有八個指令,其中兩個是 I/O 動作。作為 Turing machine 的實踐,Brainfuck 對記憶體單元作直接的存取,對應到 C 語言的概念來說,如果 char *p 指向記憶體區塊的話,Brainfuck 語言的八個指令可對照為以下: (Brainfuck vs. C) ``` Brainfuck C > ++p; < --p; + ++*p; - --*p; . putchar(*p); , *p = getchar(); [ while (*p) { ] } ``` ``` > : 將資料指標往右移 < : 將資料指標往左移 + : 將資料指標所在位置的資料加 1 - : 將資料指標所在位置的資料減 1 . : 輸出資料指標所在位置的資料 , : 讀入資料並存到指標所在位置 [ : 如果資料指標所在位置的資料為 0,則指令指標跳至往後對應的 `]` 後一條指令 ] : 如果資料指標所在位置的資料不為 0,則指令跳至往前對應的 `[` 後一條指令 ``` 也就是說,下方的 Brainfuck 程式碼: ``` +++++[-] ``` 對應為 C 語言程式碼為: ``` *p=+5; while(*p != 0){ *p--; } ``` 接著我們就來探討要如何改善此Complier,在此語言上因為極其簡易,因此許多的運算是累贅的,例如乘法、初始值等,又或是迴圈的使用,因為 Brainfuck 的迴圈終止都是仰賴 Array[0] 來判斷,在 Index 的運算上其實會有很多的不必要。 在 [brainfuck optimization strategies](http://calmerthanyouare.org/2015/01/07/optimizing-brainfuck.html) 提到以下最佳化策略: * Contraction * Clear loops * Copy loops * Multiplication loops * Operation offsets * Scan loops ### DynASM **簡介** DynASM 原本是為了 LuaJIT 所開發的,但如果有開發其他語言的 JIT compiler 的需求,也是可以使用的。 它是一個 Dynamic Assembler,主要分為兩個部份,preprocessor 與 tiny runtime。概觀如下。 ![](https://i.imgur.com/CYLax83.png) 在 `.dasc` 中會夾雜著 C code 以及組合語言,preprocessor 會依據特定語法來處理這種類型的檔案,把其中的組合語言轉換成 DynASM 內建的 method 型式,如 `dasm_put()` 等。 經過 preprocessor 產生 `.h` 檔案與其他檔案 (含 tiny runtime) 一同編譯,便可以獲得自己的 JIT compiler。 接下來,我們會觀看作業中的檔案內容並嘗試了解 DynASM 的語法。 **jit-x64.dasc** * `|` 開頭 表示這一行會被 preprocessor 做處理,後面可以接 DynASM directives 或者是組合語言。 * `|.arch x64` 指定所使用的架構。 * `|.actionlist action` 讓 preprocessor 產生類似下面的 C code。 這個 directive 在 `.dasc` 檔案中==只能出現一次==,裏面存放著是 DynADM 會使用到的 assembly。 ```clike= static const unsigned char ident[] = { /* ... */ }; ``` * `|.define PTR, rbx` 有點像 C 中的 `#define PTR rbx`。 * `|.macro ...` 讓我們定義一個巨集函數,原始的定義如下。 在這中間也可以放 C/C++ 的程式碼,但必須以 `||` 開頭。 ``` | .macro name [, param1 [, param2 [, ...]]] | /* ... */ | .endmacro ``` * `dasm_growpc()` 函數 在 DynASM 我們可能會需要用到 label 才能實現跳躍的操作,所以我們必須透過這個函數來設定可以使用的 label 數量。其 function prototype 如下。 ```clike= DASM_FDEF void dasm_growpc(Dst_DECL, unsigned int maxpc); ``` 設定完成後我們可以利用 `=>pc` 來表示某個 label,可使用的範圍是 `=>0` 到 `=>(maxpc-1)`。 **編譯產生的 jit-x64.h** * `dasm_put()` 函數 其 function protoyype 如下。 ```clike= DASM_FDEF void dasm_put(Dst_DECL, int start, ...); ``` 代表從 action list 中 index 為 start 的地方開始讀取指令,直到 DASM_STOP (255) 為止。 ## 效能分析 ### 原始版本 ```shell= $ make bench-jit-x64 Executing Brainf*ck benchmark suite. Be patient. progs/awib.b GOOD 78.7ms progs/mandelbrot.b GOOD 3252.2ms progs/hanoi.b GOOD 8158.6ms ``` ![](https://i.imgur.com/IIqPuoG.png) ### 優化版本 **Contraction** 統計 `+`、`-`、`<` 和 `>` 這類指令的連續個數,一次處理完,不像原本的版本是每遇到一個就輸出一次。 ```shell= $ make bench-jit-x64 Executing Brainf*ck benchmark suite. Be patient. progs/awib.b GOOD 45.9ms progs/mandelbrot.b GOOD 1071.1ms progs/hanoi.b GOOD 4173.3ms ``` ![](https://i.imgur.com/He7VjZh.png) **Loop 相關** Loop 的部份主要分為 clear loop、copy loop、multiplication loop 以及 scan loop。接下來將針對這些 loop 進行分析。 * Clear loop 在 Brainfuck 中如果要清除 ptr 中的值必須寫成 `[-]`,讓 ptr 每次減一直到變成零,但在實際執行的時候我們可以利用 `mov` 指令直接把他歸零就好。 Pattern: `[-]` * Copy loop 將 ptr 的值加到其他的位置上面。例如 `[->+>+<<]` 就是把 ptr 的值加到它的下一個位置跟下下一個位置。 Pattern: `[(減法) ((位移)(加法 | 減法)) (位移)]` * Multiplication loop 對其他位置加上 ptr 次的某個值。例如 `[->+++<]` 就是把下一個位置作 ptr 次的加三。 從觀察中可以發現,其實 copy loop 就是 multiplication loop 的一個集合而已,所以在優化時可以直接考慮 multiplication loop 就好。 Pattern: `[(減法) ((位移)(加法 | 減法)) (位移)]` * Scan loop 主要的工作是移動 ptr 直到遇見 0 為止。 Pattern: `[(位移)]` 再來,我們先來優化前三個 loop 看看成效如何。 根據 pattern 來看,我們所要優化的目標在遇到 `[` 時,下一個一定是 `-` 才對。因此我們可以透過這個條件來確定我們是否要優化。 檢查 loop 的程式碼如下。 ```clike= typedef struct _LOOP_OPT_INFO { int total_offset; int count; int offset[256]; int mul_value[256]; } loop_opt_info_t; int check_loop(char *p, loop_opt_info_t *info) { int count = 0, offset = 0; info->count = 0; info->total_offset = 2; if (*(p + 1) != '-') { return 0; } p += 2; while (*p != ']') { if (*p != '+' && *p != '<' && *p != '>') { return 0; } count = continuous_count(p); if (*p == '+') { info->offset[info->count] = offset; info->mul_value[info->count++] = count; } else if (*p == '<') { offset -= count; } else if (*p == '>') { offset += count; } info->total_offset += count; p += count; } ++info->total_offset; return (offset == 0); } ``` 接下來是改善後的效能。 ```shell= $ make bench-jit-x64 Executing Brainf*ck benchmark suite. Be patient. progs/awib.b GOOD 30.1ms progs/mandelbrot.b GOOD 1181.9ms progs/hanoi.b GOOD 73.1ms ``` ![](https://i.imgur.com/d4k27bw.png) ## 參考資料 * [廖健富學長 hackpad](https://hackpad.com/2015q3-Homework-4B-5I46HyqOCGJ#:h=Jit-construct) * [The Unofficial DynASM Documentation](http://corsix.github.io/dynasm-doc/reference.html)