2017q3 Homework3 (simulator)
===
contributed by <`Lihikoka`>
[GitHub](https://github.com/Lihikoka/full-stack-hello)
---
### Reviewed by `st9007a`
- computed goto 除了可以讓程式碼更簡潔,還有其他作用,應一併解釋,可以的話附上與 switch 的效能比較分析
- Fibonacci 的實作缺乏效能比較
- 尚有三種 Fibonacci 實現方式沒有實作 (tail recursion, q-matrix, fast doubling)
- 有發現到數值表達的上限 :+1:
- 發現到數值的上限後,應提出後續處理方案
- 有將 Fibonacci 加入測試流程 :+1:
---
# 開發環境
```
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 58
Model name: Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
Stepping: 9
CPU MHz: 3899.291
CPU max MHz: 3900.0000
CPU min MHz: 1600.0000
BogoMIPS: 6784.16
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 8192K
NUMA node0 CPU(s): 0-7
Operating Sysetm: Linux Mint 18.2 Cinnamon 64-bit
Cinnamon Version 3.4.6
Linux Kernel 4.8.0-53-generic
```
# 程式碼分析
## 資料結構:
整個虛擬機的精髓 virtual machine environment 如下
```clike
struct __vm_env {
vm_inst insts[INSTS_MAX_SIZE]; /* Program instructions */
vm_value cpool[CPOOL_MAX_SIZE]; /* Constant pool */
vm_value temps[TEMPS_MAX_SIZE]; /* Temporary storage */
vm_opcode_impl impl[OPCODE_IMPL_MAX_SIZE]; /* OPCODE impl */
vm_regs r;
int insts_count;
int cpool_count;
int temps_count;
};
```
* insts: 按照執行的順序儲存在連續記憶體裡的指令
* cpool: constants
* temps: stack ,是一個 array,從高記憶體位址 `temps[TEMPS_MAX_SIZE - 1]` 開始存取
* opcode_impl: 指令集的實作
```clike=
typedef struct {
size_t pc; // program counter.
size_t sp; // stack runs from the end of 'temps' region.
size_t from; // the immediate PC before last branch/return.
size_t to; // the immediate PC after last branch/return.
} vm_regs;
```
* pc: program counter ,目前程式執行到的指令位址,與 `vm_env.insts` 搭配使用
* sp: stack pointer
## 程式流程
主程式為 `driver.c` ,先從沒有給參數的 case 開始看
```clike=
vm_env *env = vm_new();
assemble_from_fd(env, in_fd);
hook_opcodes(env);
vm_run(env);
vm_free(env);
```
可以得知流程為:
1. 先創造一個指向初始化後的 `vm_env` 的指標 `env`
2. 接著 `assemble_from_fd` 會從 `.s` 檔讀入每一行指令並放進 `env`
3. `hook_opcodes` 會將每個指令的名稱與實作「 hook 」在一起
4. 然後由 `vm_run` 來執行所有 `env->insts` 裡的指令
5. 最後 `vm_free` 將記憶體釋放。
### Computed goto 技巧
opcode.h:
```clike=
#define OP_LABELS &&OP_HALT, &&OP_ADD, &&OP_SUB, &&OP_PRINT, &&OP_JLT, \
&&OP_JLE, &&OP_JZ, &&OP_JGE, &&OP_JGT, \
&&OP_JNZ, &&OP_JMP, &&OP_MUL, &&OP_DIV, \
&&OP_MOD, &&OP_CALL, &&OP_RET, &&OP_AND, \
&&OP_OR, &&OP_NOT, &&OP_XOR, &&OP_LSL, \
&&OP_LSR, &&OP_ASR,
```
vm.c:
```clike=
#define OPCODE env->insts[env->r.pc]
#define BEGIN_OPCODES \
const static void *labels[] = {OP_LABELS}; \
goto *labels[OPCODE.opcode]
#define DISPATCH \
do { \
++env->r.pc; \
goto *labels[OPCODE.opcode]; \
} while (0)
#define GOTO(n) \
do { \
env->r.from = env->r.pc; \
env->r.to = n; \
env->r.pc = n; \
goto *labels[OPCODE.opcode]; \
} while (0)
void vm_run(vm_env *env)
{
BEGIN_OPCODES;
OP(ADD) : VM_CALL_HANDLER();
OP(SUB) : VM_CALL_HANDLER();
OP(MUL) : VM_CALL_HANDLER();
OP(DIV) : VM_CALL_HANDLER();
OP(MOD) : VM_CALL_HANDLER();
OP(AND) : VM_CALL_HANDLER();
OP(OR) : VM_CALL_HANDLER();
OP(NOT) : VM_CALL_HANDLER();
OP(XOR) : VM_CALL_HANDLER();
OP(LSL) : VM_CALL_HANDLER();
OP(LSR) : VM_CALL_HANDLER();
OP(ASR) : VM_CALL_HANDLER();
OP(PRINT) : VM_CALL_HANDLER();
OP(JLT) : VM_JLT();
OP(JLE) : VM_JLE();
OP(JZ) : VM_JZ();
OP(JGE) : VM_JGE();
OP(JGT) : VM_JGT();
OP(JNZ) : VM_JNZ();
OP(JMP) : GOTO(OPCODE.op1.value.id);
OP(CALL) : VM_CALL(OPCODE.op1.value.id);
OP(RET) : VM_RET();
OP(HALT) : goto terminate;
END_OPCODES;
terminate:
return;
}
```
因為都是在 `run` 函式內,所以如上方程式碼這樣定義 label 並 `goto` 這些 label 並執行對應的程式碼,雖然用 switch 也可以達到相同的功能,但是 computed goto 技巧可以讓程式碼看起來比較簡潔。
# 修改程式碼接受輸入參數
首先在 `vm.c` 中加入設定 temp 的值的函式,注意到因為要在外部呼叫,因此不加上 static qualifier
```clike=
inline void vm_set_temp_value(vm_env *env, int pos, int n)
{
vm_value v = {.type = INT};
v.value.vint = n;
env->temps[pos] = v;
}
```
在 `drive.c` 判斷輸入的 `else if (argv[i][0] == '-')` 之前加入接受輸入參數的 case
```clike=
else if (!strcmp(argv[i], "--input")) {
if (!argv[i + 1])
FATAL(-1, "Missing an argument after --input\n");
input_value = atoi(argv[++i]);
}
```
在 `drive.c` 的每個 `vm_env *env = vm_new();` 的下一行設定輸入參數,儲存參數的位置都是在 `temp[0]`
```clike=
vm_set_temp_value(env, 0, input_value);
```
在 `Makefile` 中加入 `fib` 選項與相關操作:
```
TEMP ?= 20
FILE_FIB_ITER ?= ./tests/fib-iterative.s
FILE_FIB_RECU ?= ./tests/fib-recursive.s
fib: $(EXEC)
@./$(EXEC) --input $(TEMP) $(FILE_FIB_ITER)
@./$(EXEC) --input $(TEMP) $(FILE_FIB_RECU)
```
參考資料: [st9007a 同學共筆](https://hackmd.io/s/Hkf6pkPAb#fibonacci-數列)
# Fabonacci 數列
## iterative version
### C 語言程式碼
```clike=
int fib_iterative(int n) {
if (n == 0)
return n;
if (n == 1)
return n;
int first = 0;
int second = 1;
int result = 0;
int i = n - 1;
while(i > 0) {
i--;
result = first + second;
first = second;
second = result;
}
return result;
}
```
### 對應的組合語言
fib-iterative.s
```=
; if (n == 0) return n
jz #0 #15
; if (n == 1) return n
sub #0 $1 #5
jz #5 #15
; #1: first = 0
; #2: second = 1
; #3: result = 0
; #4: i = n - 1
or $0 $0 #1
or $0 $1 #2
or $0 $0 #3
sub #0 $1 #4
; iteration
jz #4 #13
sub #4 $1 #4
add #1 #2 #3
or $0 #2 #1
or $0 #3 #2
call #7
print #3
halt
print #0
halt
```
## recursive version
### C 語言程式碼
```clike=
int fib_recursive(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib_recursive(n - 1) + fib_recursive(n - 2);
}
```
### 對應的組合語言
fib-recursive.s
```=
; #0: n
; #1: result
; #2: temp
; initialize result to 0
or $0 $0 #1
; enter to recursion
call #4
; print result
print #1
halt
; recursion
; if #0 == 1 is true, #1 = #1 + 1, then return
xor $1 #0 #2
jz #2 #7
jnz #2 #9
add #1 $1 #1
ret
; if #0 == 0 is true, then return
xor $0 #0 #2
jz #2 #12
jnz #2 #13
ret
; fib(n-1)
sub #0 $1 #0
call #4
; fib(n-2)
sub #0 $1 #0
call #4
; compensate n the subtracted 2
add #0 $2 #0
ret
```
### Overflow
```
$ ./as_exec tests/fib-iterative.s --input 46
1836311903
$ ./as_exec tests/fib-iterative.s --input 47
-1323752223
```
發現輸入值超過 46 後就會導致 integer overflow
# 修改自動測試程式
在 `Makefile` 中有提供 test 功能,是利用 `runner.py` 來抓每個 `.s` 檔來執行並和對應的預期執行結果 `.expected` 檔來作比較。
將 Makefile 中 fib: 選項的輸入參數設為 `40` 並將其答案 `6765` 寫入檔案 `fib-iterative.expected` 和 `fib-recursive.expected`。
`runner.py` 取得執行結果的 function 都是直接執行,沒有輸入參數,因應 `fib-iterative.s` 和 `fib-recursive.s` 需要接受輸入參數,將 function 修改成:
```python=
def get_exec_output(testfile):
if testfile == 'tests/fib-iterative' or testfile == 'tests/fib-recursive':
return subprocess.check_output(['./as_exec', '%s.s' % testfile, '--input', '20'])
else:
return subprocess.check_output(['./as_exec', '%s.s' % testfile])
```
研究了一下 `subprocess` 模組中 `check_output` ,用途是產生一個執行第一個參數所代表的指令的 child process,並檢查 return 值是否為 0 ,若不為 0 則發生 Error
執行 `make test`:
```
.FF....
======================================================================
FAIL: test_fib-iterative (__main__.TestRunner)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests/runner.py", line 41, in test
self.assertEqual(got, expected)
AssertionError: '6765\n' != '6765'
======================================================================
FAIL: test_fib-recursive (__main__.TestRunner)
----------------------------------------------------------------------
Traceback (most recent call last):
File "tests/runner.py", line 41, in test
self.assertEqual(got, expected)
AssertionError: '6765\n' != '6765'
----------------------------------------------------------------------
Ran 7 tests in 0.001s
FAILED (failures=2)
Makefile:49: recipe for target 'test' failed
make: *** [test] Error 1
```
發現結果說 `6765\n != 6765` ,然後去 `opcode.c` 看 `print` 指令的實作:
```clike=
static void print_impl(VM_HANDLER_ARGS)
{
switch (VM_T(op1)) {
case INT:
printf("%d\n", op1->value.vint);
break;
case STR:
printf("%s\n", op1->value.vstr);
break;
default:
break;
}
}
```
當中有換行符號 「`\n`」 ,但是其他測試檔 (例如 `mul.s`) 中也有包含 `print` 指令,但是卻沒有出錯,於是發現 `mul.expected` 中最後一行有 blank line ,於是在 `fib-iterative.expected` 和 `fib-recursive.expected` 的最後面也加入 blank line,並執行 `make test` 得到結果:
```
.......
----------------------------------------------------------------------
Ran 7 tests in 0.001s
OK
```