Lua一直把虛擬機執行代碼的效率作為一個非常重要的設計目標。而採用什麼樣的指令系統的對於虛擬機的執行效率來說至關重要。
Stack based vs Register based VM
根據指令獲取操作數方式的不同,我們可以把虛擬機的實現分為stack based和register based。
Stack based vm
對於大多數的虛擬機,比如JVM,Python,都採用傳統的stack based vm。
Stack based vm的指令一般都是在當前stack中獲取和保存操作數的。比如一個簡單的加法賦值運算:a=b+c,對於stack based vm,一般會被轉化成如下的指令:
- push b; // 將變量b的值壓入stack
- push c; // 將變量c的值壓入stack
- add; // 將stack頂部的兩個值彈出後相加,將結果壓入stack
- mov a; // 將stack頂部結果放到a中
由於Stack based vm的指令都是基於當前stack來查找操作數的,這就相當於所有操作數的存儲位置都是運行期決定的,在編譯器的代碼生成階段不需要額外為在哪裡存儲操作數費心,所以stack based的編譯器實現起來相對比較簡單直接。也正因為這個原因,每條指令佔用的存儲空間也比較小。
但是,對於一個簡單的運算,stack based vm會使用過多的指令組合來完成,這樣就增加了整體指令集合的長度。vm會使用同樣多的迭代次數來執行這些指令,這對於效率來說會有很大的影響。並且,由於操作數都要放到stack上面,使得移動這些操作數的內存複製大大增加,這也會影響到效率。
Register based vm
Lua 採用的是register based vm。
Register based vm的指令都是在已經分配好的寄存器中存取操作數。對於上面的運算,register based vm一般會使用如下的指令:
- add abc; // 將b與c對應的寄存器的值相加,將結果保存在a對應的寄存器中
Register based vm的指令可以直接對應標準的3地址指令,用一條指令完成了上面多條指令的計算工作,並且有效地減少了內存複製操作。這樣的指令系統對於效率有很大的幫助。
Lua的指令使用一個32bit的unsigned integer表示。所有指令的定義都在lopcodes.h文件中,使用一個enum OpCode代表指令類型。在lua5.2中,總共有40種指令(id從0到39)。根據指令參數的不同,可以將所有指令分為4類:
除了sBx之外,所有的指令參數都是unsigned integer類型。sBx可以表示負數,但表示方法比較特殊。sBx的18bit可表示的最大整數為262143,這個數的一半131071用來表示0,所以-1可以表示為-1+131071,也就是131070,而+1可以表示為+1+131071,也就是131072 。
ABC一般用來存放指令操作數據的地址,而地址可以分成3種:
- 寄存器id
- 常量表id
- upvalue id
Lua使用當前函數的stack作為寄存器使用,寄存器id從0開始。當前函數的stack與寄存器數組是相同的概念。stack(n)其實就是register(n)。
每一個函數prototype都有一個屬於本函數的常量表,用於存放編譯過程中函數所用到的常量。常量表可以存放nil,boolean,number和string類型的數據,id從1開始。
每一個函數prototype中都有一個upvalue描述表,用於存放在編譯過程中確定的本函數所使用的upvalue的描述。在運行期,通過OP_CLOSURE指令創建一個closure時,會根據prototype中的描述,為這個closure初始化upvalue表。upvalue本身不需要使用名稱,而是通過id進行訪問。
A被大多數指令用來指定計算結果的目標寄存器地址。很多指令使用B或C同時存放寄存器地址和常量地址,並通過最左面的一個bit來區分。在指令生成階段,如果B或C需要引用的常量地址超出了表示範圍,則首先會生成指令將常量裝載到臨時寄存器,然後再將B或C改為使用該寄存器地址。
在lopcodes.h中,對於每個指令,在源碼註釋中都有簡單的操作描述。本文接下來將針對每一個指令做更詳細的描述,並給出關於這個指令的示例代碼。示例代碼可以幫助我們構建出一個指令使用的具體上下文,有助於進一步理解指令的作用。對指令上下文的理解還可以作為進一步研究lua的編譯和代碼生成系統的基礎。
在分析過程中,我們使用luac來顯示示例代碼所生成的指令。luac的具體使用方式為:
- luac -l -l test.lua
No comments:
Post a Comment