種一棵樹最好的時間是十年前, 其次是如今. php
0x00 幾點概念解釋器指令寄存器棧局部變量表操做數棧0x01 寄存器仍是棧場景寄存器方案棧方案混合方案, 結合寄存器和棧的方案(即 JVM 的方案)簡單比較0x02 解釋器核心0x03 簡單代碼實現寄存器棧混合關於上方代碼0x04 小結0x05 預告0x06 FAQjava
解釋器, 是一種程序,可以把編程語言一行一行解釋運行。解釋器像是一位「中間人」,每次運行程序時都要先轉成另外一種語言再做運行,所以解釋器的程序運行速度比較緩慢。它不會一次把整個程序翻譯出來,而是每翻譯一行程序敘述就馬上運行,而後再翻譯下一行,再運行,如此不停地進行下去。c++
一些相關的概念, 彙編指令, JVM 字節碼指令.
指令通常很簡單, 描述了一個具體的操做. 好比
彙編指令
mov &ex, 1 => 將整數 1 放到寄存器 ex 裏.
字節碼指令
bpush 1 => 將 byte 1 放到操做數棧頂.git
簡單來講寄存器就是個 Map. 能夠根據寄存器地址(key)對其進行存取(value). 主要操做 get(key) , put(key,value)github
簡單來說, 後進先出, 只支持 push 和 pop 操做.
push : 將某個值放到棧的棧頂, 棧大小加 1.
pop : 將棧的棧頂的值弄出來, 棧大小減 1. web
棧幀內部的數據結構, 是個數組. 經過數組位置訪問, e.g array[0], array[1]. 換個角度來看, 其實能夠認爲是個特殊的寄存器. 只不過 key 是下標而已. 編程
棧幀內部數據結構, 同棧. 數組
脫離業務場景的技術設計都是耍流氓. -- 尼古拉斯.趙四bash
僞代碼以下.數據結構
int a = 1 + 1;
int b = 2 + 2;
int c = 0;
int d = b - a;
int d = d - c;
println(d)
複製代碼
若是正確運行, 必然輸出 2.
自動化的前提是能手動化. 因此人肉編譯一下吧.
生成指令格式, inst [value|address]+
e.g
mov &0 1 => 把數值 1 放到寄存器 0 裏
add &3 &0 &1 &2 => 把 寄存器 0 ,寄存器 1, 寄存器 2 裏的值相加, 並把結果放到 寄存器 3 裏.
sub &3 &0 &1 &2 => 把 寄存器 0 裏的值 減去 寄存器 1, 寄存器 2 裏的值, 並把結果放到 寄存器 3 裏.
println &3 => 取出 寄存器 3 的值 並輸出.
複製代碼
生成的指令代碼以下. 爲方便理解, 雙斜槓以後爲註釋. 代表操做以後寄存器或棧的狀態
mov &0 1 // {0:1}
mov &1 1 // {0:1, 1:1}
add &2 &0 &1 // {0:1, 1:1, 2:2}
mov &3 2 // {0:1, 1:1, 2:2, 3:2}
mov &4 2 // {0:1, 1:1, 2:2, 3:2, 4:2}
add &5 &3 &4 // {0:1, 1:1, 2:2, 3:2, 4:2, 5:4}
mov &6 0 // {0:1, 1:1, 2:2, 3:2, 4:2, 5:4, 6:0}
sub &7 &5 &2 // {0:1, 1:1, 2:2, 3:2, 4:2, 5:4, 6:0, 7:2}
sub &8 &7 &6 // {0:1, 1:1, 2:2, 3:2, 4:2, 5:4, 6:0, 7:2, 8:2}
println &8 // {0:1, 1:1, 2:2, 3:2, 4:2, 5:4, 6:0, 7:2, 8:2}
複製代碼
生成指令格式 inst [value]{0,1}
e.g
push 1 => 將數值 1 推到棧頂. (..) -> (..,1)
add => 將棧頂的兩個數值相加, 並將結果放到棧頂. (..,v1,v2) -> (..,v1+v2)
sub => 假設棧頂值爲v2, (..,v1,v2) -> (..,v1-v2)
swap => 交換棧頂兩個元素 (v1,v2) -> (v2,v1)
swap1 => 交換棧上第二,第三位置 (v1,v2,x) -> (v2,v1,x)
println => 棧頂數值出站, 並將結果輸出. (..,1) -> (..)
複製代碼
生成的指令代碼以下. 爲方便理解, 雙斜槓以後爲註釋. 代表操做以後寄存器或棧的狀態
push 1 // (1)
push 1 // (1, 1)
add // (2)
push 2 // (2, 2)
push 2 // (2, 2, 2)
add // (2, 4)
push 0 // (2, 4, 0)
swap // (2, 0, 4)
swap1 // (0, 2, 4)
swap // (0, 4, 2)
sub // (0, 2)
swap // (2, 0)
sub // (2)
println // ()
複製代碼
e.g:
load => 從寄存器中取值並 push 到操做數棧中.
store => 操做數棧頂數值出棧, 並存放到寄存器中.
複製代碼
生成的指令代碼以下. 爲方便理解, 雙斜槓以後爲註釋. 代表操做以後寄存器或棧的狀態
push 1 // (1) {}
push 1 // (1, 1) {}
add // (2) {}
store &0 // () {0:2}
push 2 // (2) {0:2}
push 2 // (2, 2) {0:2}
add // (4) {0:2}
store &1 // () {0:2, 1:4}
push 0 // (0) {0:2, 1:4}
store &2 // () {0:2, 1:4, 2:0}
load &1 // (4) {0:2, 1:4, 2:0}
load &0 // (4, 2) {0:2, 1:4, 2:0}
sub // (2) {0:2, 1:4, 2:0}
store &3 // () {0:2, 1:4, 2:0, 3:2}
load &3 // (2) {0:2, 1:4, 2:2, 3:2}
load &2 // (2, 0) {0:2, 1:4, 2:2, 3:2}
sub // (2) {0:2, 1:4, 2:2, 3:2}
store $4 // () {0:2, 1:4, 2:2, 3:2, 4:2}
load $4 // (2) {0:2, 1:4, 2:2, 3:2, 4:2}
println // () {0:2, 1:4, 2:2, 3:2, 4:2}
複製代碼
簡單場景下, 三種方案都可知足需求.
其中寄存器方案對應這計算機物理實現. CPU 的處理即是基於寄存器的. 優勢性能高, 數據的搬運次數少, 缺點指令複雜.
純粹基於棧的方案, 貌似沒有, 由於只有 push pop 兩種操做, 在局部變量較多的狀況下, 數據須要頻繁搬運. 優勢是指令簡單. 方便移植.
混合方案, 集兩家之長, 在移植性和效率上的折中方案.
如上篇預告. 解釋器的核心是一個循環.
do {
獲取下一個指令
解釋指令
} while (還有指令);
複製代碼
架構圖以下
// 核心循環
public void run() {
List<Inst> insts = genInsts();
int size = insts.size();
int pc = 0;
while (pc < size) {
Inst inst = insts.get(pc);
inst.execute();
pc++;
}
}
// Add 指令實現
class AddInst implements Inst {
public final Integer targetAddress;
public final Integer[] sourceAddresses;
AddInst(Integer targetAddress, Integer... sourceAddresses) {
this.targetAddress = targetAddress;
this.sourceAddresses = sourceAddresses;
}
@Override
public void execute() {
int sum = 0;
for (Integer sourceAddress : sourceAddresses) {
sum += RegisterDemo.REGISTER.get(sourceAddress);
}
RegisterDemo.REGISTER.put(targetAddress, sum);
}
}
複製代碼
代碼地址: 寄存器 DEMO
// 核心循環
public void run() {
List<Inst> insts = genInsts();
int size = insts.size();
int pc = 0;
while (pc < size) {
Inst inst = insts.get(pc);
inst.execute();
pc++;
}
}
// Add 指令實現
class AddInst implements Inst {
@Override
public void execute() {
Integer v2 = StackDemo.STACK.pop();
Integer v1 = StackDemo.STACK.pop();
StackDemo.STACK.push(v1 + v2);
}
}
複製代碼
地址: 棧 DEMO
// 核心循環
public void run() {
List<Inst> insts = genInsts();
int size = insts.size();
int pc = 0;
while (pc < size) {
Inst inst = insts.get(pc);
inst.execute();
pc++;
}
}
// Add 指令實現
class AddInst implements Inst {
@Override
public void execute() {
Integer v2 = HybridDemo.STACK.pop();
Integer v1 = HybridDemo.STACK.pop();
HybridDemo.STACK.push(v1 + v2);
}
}
複製代碼
地址: 混合 DEMO
針對具體場景實現, 恰好能用. 三個方案, 每一個方案均不超過 100 行代碼. 回上篇問題, 實現一個簡單的解釋器, 10 分鐘夠不夠?
天然是夠的, 有興趣不妨試着寫一下.
本文討論瞭解釋器實現的三種方案, 並就簡單的案例分別實現了相應的解釋器.
mini-jvm 即是從這簡單的核心中慢慢擴展而來.
因爲 JVM 選擇的是混合方案, 後續的重點就只會在混合方案上了.
!!! 解釋器的核心實現尤其重要, 若是此文並無並無讓讀者理解, 必定是文章的問題, 歡迎提出你的問題, 已迭代此文.