Lua一直把虛擬機執行代碼的效率作爲一個非常重要的設計目標。而採用什麼樣的指令系統的對於虛擬機的執行效率來說至關重要。
Stack based vs Register based VM
根據指令獲取操作數方式的不同,我們可以把虛擬機的實現分爲stack based和register based。
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 a b c; // 將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
每一個函數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
Lua首先將源程序編譯成爲字節碼,然後交由虛擬機解釋執行.對於每一個函數,Lua的編譯器將創建一個原型(prototype),它由一組指令及其使用到的常量組成[1].最初的Lua虛擬機是基於棧的.到1993年,Lua5.0版本,採用了基於寄存器的虛擬機,使得Lua的解釋效率得到提升,
1、指令系統
與虛擬機和指令相關的文件主要有兩個: lopcodes.c和lvm.c.從名稱可以看出來,這兩個文件分別用於描述操作碼(指令)和虛擬機.
(1)指令列舉
Lua共有38條指令,在下面兩處地方分別描述了這些指令的名稱和模式,如下:lopcodes.c:16
const char*const luaP_opnames[NUM_OPCODES+1] = {
"MOVE",
"LOADK",
"LOADBOOL",
"LOADNIL",
"GETUPVAL",
"GETGLOBAL",
"GETTABLE",
"SETGLOBAL",
"SETUPVAL",
"SETTABLE",
"NEWTABLE",
"SELF",
"ADD",
"SUB",
"MUL",
"DIV",
"MOD",
"POW",
"UNM",
"NOT",
"LEN",
"CONCAT",
"JMP",
"EQ",
"LT",
"LE",
"TEST",
"TESTSET",
"CALL",
"TAILCALL",
"RETURN",
"FORLOOP",
"FORPREP",
"TFORLOOP",
"SETLIST",
"CLOSE",
"CLOSURE",
"VARARG",
NULL
};
#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) |((c)<<2) | (m))
const lu_byte luaP_opmodes[NUM_OPCODES] = {
/* T A B C mode opcode */
opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_MOVE */
,opmode(0, 1, OpArgK, OpArgN, iABx) /* OP_LOADK */
,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_LOADBOOL */
,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_LOADNIL */
,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_GETUPVAL */
,opmode(0, 1, OpArgK, OpArgN, iABx) /* OP_GETGLOBAL */
,opmode(0, 1, OpArgR, OpArgK, iABC) /* OP_GETTABLE */
,opmode(0, 0, OpArgK, OpArgN, iABx) /* OP_SETGLOBAL */
,opmode(0, 0, OpArgU, OpArgN, iABC) /* OP_SETUPVAL */
,opmode(0, 0, OpArgK, OpArgK, iABC) /* OP_SETTABLE */
,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_NEWTABLE */
,opmode(0, 1, OpArgR, OpArgK, iABC) /* OP_SELF */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_ADD */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_SUB */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_MUL */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_DIV */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_MOD */
,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_POW */
,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_UNM */
,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_NOT */
,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_LEN */
,opmode(0, 1, OpArgR, OpArgR, iABC) /* OP_CONCAT */
,opmode(0, 0, OpArgR, OpArgN, iAsBx) /* OP_JMP */
,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_EQ */
,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_LT */
,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_LE */
,opmode(1, 1, OpArgR, OpArgU, iABC) /* OP_TEST */
,opmode(1, 1, OpArgR, OpArgU, iABC) /* OP_TESTSET */
,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_CALL */
,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_TAILCALL */
,opmode(0, 0, OpArgU, OpArgN, iABC) /* OP_RETURN */
,opmode(0, 1, OpArgR, OpArgN, iAsBx) /* OP_FORLOOP */
,opmode(0, 1, OpArgR, OpArgN, iAsBx) /* OP_FORPREP */
,opmode(1, 0, OpArgN, OpArgU, iABC) /* OP_TFORLOOP */
,opmode(0, 0, OpArgU, OpArgU, iABC) /* OP_SETLIST */
,opmode(0, 0, OpArgN, OpArgN, iABC) /* OP_CLOSE */
,opmode(0, 1, OpArgU, OpArgN, iABx) /* OP_CLOSURE */
,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_VARARG */
};
前面一個數組容易理解,表示了每條指令的名稱.後面一個數組表示的是指令的模式.奇怪的符號讓人有些費解.在看模式之前,首先來看Lua指令的格式:
如上圖,Lua的指令可以分成三種形式.即在上面的模式數組中也可以看到的iABC, iABx和 iAsBx.對於三種形式的指令來說,前兩部分都是一樣的,分別是6位的操作碼和8位A操作數;區別在於,後面部是分割成爲兩個長度爲9位的操作符(B, C),一個無符號的18位操作符Bx還是有符號的18位操作符sBx.這些定義的代碼如下:
lopcodes.c : 34
/*
** size and position of opcode arguments.
*/
#define SIZE_C 9
#define SIZE_B 9
#define SIZE_Bx (SIZE_C + SIZE_B)
#define SIZE_A 8
#define SIZE_OP 6
#define POS_OP 0
#define POS_A (POS_OP + SIZE_OP)
#define POS_C (POS_A + SIZE_A)
#define POS_B (POS_C + SIZE_C)
#define POS_Bx POS_C
(2)指令的操作模式
Lua使用一個字節來表示指令的操作模式.具體的含義如下:
1)使用最高位來表示是否是一條測試指令.之所以將這一類型的指令特別地標識出來,是因爲Lua的指令長度是32位,對於分支指令來說,要想在這32位中既表示兩個操作數來做比較,同時還要表示一個跳轉的地址,是很困難的.
因此將這種指令分成兩條,第一條是測試指令,緊接着一條無條件跳轉.如果判斷條件成立則將PC(Program Counter,指示下一條要執行的指令)加一,跳過下一條無條件跳轉指令,繼續執行;否則跳轉.
2)第二位用於表示A操作數是否被設置
3) 接下來的二位用於表示操作數B的格式,OpArgN表示操作數未被使用, OpArgU表示操作數被使用(立即數?), OpArgR表示表示操作數是寄存器或者跳轉的偏移量, OpArgK表示操作數是寄存器或者常量.
2、Lua虛擬機的體系結構
給出Lua虛擬機的體系結構圖(根據源代碼分析得出):
首先,我們注意到,Lua的解釋器還是一個以棧爲中心的結構.
在lua_State這個結構中,有許多個字段用於描述這個結構.stack用於指向絕對棧底,而base指向了當前正在執行的函數的第一個參數,而top指向棧頂的第一個空元素.
我們可以看到,這個體系結構中並沒有獨立出來的寄存器.
從以下代碼來看:
lvm.c:343
#define RA(i) (base+GETARG_A(i))
/* to be used after possible stack reallocation */
#define RB(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgR,base+GETARG_B(i))
#define RC(i) check_exp(getCMode(GET_OPCODE(i)) == OpArgR,base+GETARG_C(i))
#define RKB(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgK, /
ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i))
#define RKC(i) check_exp(getCMode(GET_OPCODE(i)) == OpArgK, /
ISK(GETARG_C(i)) ? k+INDEXK(GETARG_C(i)) : base+GETARG_C(i))
#define KBx(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgK,k+GETARG_Bx(i))
當指令操作數的類型是寄存器時,它的內容是以base爲基址在棧上的索引值.如圖所示.寄存器實際是base之上棧元素的別名;當指令操作數的類型的常數時,它首先判斷B操作數的最位是否爲零.如果是零,則按照和寄存器的處理方法一樣做,如果不是零,則在常數表中找相應的值.
我們知道Lua中函數的執行過程是這樣的.首先將函數壓棧,然後依次將參數壓棧,形成圖中所示的棧的內容.因此R[0]到R[n]也分別表示了Arg[1]到Arg[N+1].在第一個參數之下,就是當前正在執行的函數,對於Lua的函數(相對C函數)來說,它是指向類型爲 Prototype的TValue,在Prototype中字段code指向了一個數組用來表示組成這個函數的所有指令,字段k指向一個數組來表示這個函數使用到的所有常量.最後,Lua在解釋執行過程中有專門的變量pc來指向下一條要執行的指令.
3、指令解釋
有了前面對指令格式和體系結構的介紹,
現在我們可以進入正題,來看看Lua的指令是如何執行的了.主函數如下:
(1)執行lua指令函數
lvm.c:373
void luaV_execute (lua_State *L, int nexeccalls){
LClosure *cl;
StkId base;
TValue *k;
const Instruction *pc;
reentry: /* entry point */
lua_assert(isLua(L->ci));
pc = L->savedpc;
cl = &clvalue(L->ci->func)->l;
base = L->base;
k = cl->p->k;
這是最開始的初始化過程.
其中, pc被初始化成爲了L->savedpc,base被初始化成爲了L->base,即程序從L->savedpc開始執行(在下一篇專題中,將會介紹到 L->savedpc在函數調用的預處理過程中指向了當前函數的code),而L->base指向棧中當前函數的下一個位置.cl表示當前正在執行閉包(當前可以理解成爲函數),k指向當前閉包的常量表.
接下來(注意,爲了專注主要邏輯,我將其中用於Debugger支持,斷言等代碼省略了):
/* main loop of interpreter */
for (;;) {
const Instruction i = *pc++;
StkId ra;
/* 省略Debugger支持和Coroutine支持*/
/* warning!! several calls may realloc the stack andinvalidate `ra' */
ra = RA(i);
/* 省略斷言 */
switch (GET_OPCODE(i)) {
進入到解釋器的主循環,處理很簡單,取得當前指令,pc遞增,初始化ra,然後根據指令的操作碼進行選擇.接下來的代碼是什麼樣的,估計大家都能想到,一大串的case來指示每條指令的執行.具體的實現可以參考源碼,在這裏不對每一條指令展開,只是對其中有主要的幾類指令進行說明:
傳值類的指令,與MOVE爲代表:
lvm.c:403
case OP_MOVE: {
setobjs2s(L, ra, RB(i));
continue;
}
lopcodes:154
OP_MOVE,/* A B R(A) :=R(B) */
lobject.h:161
#define setobj(L,obj1,obj2) /
{ const TValue *o2=(obj2); TValue *o1=(obj1); /
o1->value = o2->value; o1->tt=o2->tt; /
checkliveness(G(L),o1); }
/*
** different types of sets, according to destination
*/
/* from stack to (same) stack */
#define setobjs2s setobj
從註釋來看,這條指令是將操作數A,B都做爲寄存器,然後將B的值給A.而實現也是簡單明瞭,只使用了一句.宏展開以後,可以看到, R[A],R[B]的類型是TValue,只是將這兩域的值傳過來即可.對於可回收對象來說,真實值不會保存在棧上,所以只是改了指針,而對於非可回收對象來說,則是直接將值從R[B]賦到R[A].
數值運算類指令,與ADD爲代表:
lvm.c:470
case OP_ADD: {
arith_op(luai_numadd, TM_ADD);
continue;
}
lvm.c:360
#define arith_op(op,tm) { /
TValue *rb = RKB(i); /
TValue *rc = RKC(i); /
if (ttisnumber(rb) &&ttisnumber(rc)) { /
lua_Number nb =nvalue(rb), nc = nvalue(rc); /
setnvalue(ra, op(nb,nc)); /
} /
else /
Protect(Arith(L, ra, rb,rc, tm)); /
}
lopcodes.c:171
OP_ADD,/* A B C R(A) := RK(B) +RK(C) */
如果兩個操作數都是數值的話,關鍵的一行是:
setnvalue(ra,op(nb,nc));
即兩個操作數相加以後,把值賦給R[A].值得注意的是,操作數B,C都是RK,即可能是寄存器也可能是常量,這最決於最B和C的最高位是否爲1,如果是1,則是常量,反之則是寄存器.具體可以參考宏ISK的實現.
如果兩個操作數不是數值,即調用了Arith函數,它嘗試將兩個操作轉換成數值進行計算,如果無法轉換,則使用元表機制.該函數的實現如下:
lvm.c:313
static void Arith (lua_State *L, StkId ra, const TValue *rb,
const TValue *rc, TMS op) {
TValue tempb, tempc;
const TValue *b, *c;
if ((b = luaV_tonumber(rb, &tempb)) != NULL &&
(c = luaV_tonumber(rc, &tempc)) != NULL) {
lua_Number nb = nvalue(b), nc = nvalue(c);
switch (op) {
case TM_ADD: setnvalue(ra, luai_numadd(nb, nc));break;
case TM_SUB: setnvalue(ra, luai_numsub(nb, nc));break;
case TM_MUL: setnvalue(ra, luai_nummul(nb, nc));break;
case TM_DIV: setnvalue(ra, luai_numdiv(nb, nc));break;
case TM_MOD: setnvalue(ra, luai_nummod(nb, nc));break;
case TM_POW: setnvalue(ra, luai_numpow(nb, nc));break;
case TM_UNM: setnvalue(ra, luai_numunm(nb));break;
default: lua_assert(0); break;
}
}
else if (!call_binTM(L, rb, rc, ra, op))
luaG_aritherror(L, rb, rc);
}
在上面call_binTM用於調用到元表中的元方法,因爲在Lua以前的版本中元方法也被叫做tag method, 所以函數最後是以TM結尾的.
lvm:163
static int call_binTM (lua_State *L, const TValue *p1, const TValue *p2,
StkId res, TMS event) {
const TValue *tm = luaT_gettmbyobj(L, p1, event); /* try firstoperand */
if (ttisnil(tm))
tm = luaT_gettmbyobj(L, p2, event); /* try secondoperand */
if (!ttisfunction(tm)) return 0;
callTMres(L, res, tm, p1, p2);
return 1;
}
在這個函數中,試着從二個操作數中找到其中一個操作數的元方法(第一個操作數優先),這裏event表示具體哪一個元方法,找到了之後,再使用函數callTMres()去調用相應的元方法. callTMres()的實現很簡單,只是將元方法,第一,第二操作數先後壓棧,再調用並取因返回值.具體如下:
lvm.c:82
static void callTMres (lua_State *L, StkId res, const TValue *f,
const TValue *p1, const TValue *p2) {
ptrdiff_t result = savestack(L, res);
setobj2s(L, L->top, f); /* push function */
setobj2s(L, L->top+1, p1); /* 1st argument */
setobj2s(L, L->top+2, p2); /* 2nd argument */
luaD_checkstack(L, 3);
L->top += 3;
luaD_call(L, L->top - 3, 1);
res = restorestack(L, result);
L->top--;
setobjs2s(L, res, L->top);
}
邏輯運算類指令,與EQ爲代表:
lvm.c:541
case OP_EQ: {
TValue *rb = RKB(i);
TValue *rc = RKC(i);
Protect(
if (equalobj(L, rb, rc)== GETARG_A(i))
dojump(L,pc, GETARG_sBx(*pc));
)
pc++;
continue;
}
lopcodes.c:185
OP_EQ,/* A B C if ((RK(B) == RK(C)) ~= A)then pc++ */
在這條指令實現的過程中,equalobj與之前的算術運算類似,讀者可以自行分析.關鍵看它是如果實現中跳轉的,如果RK[B]==RK[C]並且A爲1的情況下(即條件爲真),則會使用pc取出下一條指令,調用dojump進行跳轉,否則pc++,掛空緊接着的無條件跳轉指令. dojump的實現如下:
lvm.c:354
#define dojump(L,pc,i) {(pc) += (i); luai_threadyield(L);}
luai_threadyield只是順序地調用lua_unlock和lua_lock,這裏爲釋放一次鎖,使得別的線程可以得到調度.
函數調用類指令,與CALL爲代表:
lvm.c:582
case OP_CALL: {
int b = GETARG_B(i);
int nresults = GETARG_C(i) - 1;
if (b != 0) L->top = ra+b; /* else previous instruction set top */
L->savedpc = pc;
switch (luaD_precall(L, ra,nresults)) {
case PCRLUA: {
nexeccalls++;
gotoreentry; /* restart luaV_execute over new Lua function */
}
case PCRC: {
/* it was aC function (`precall' called it); adjust results */
if (nresults>= 0) L->top = L->ci->top;
base =L->base;
continue;
}
default: {
return; /* yield */
}
}
}
lopcodes.c:192
OP_CALL,
/* A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */
這一條指令將在下一個介紹Lua函數調用規範的專題中詳細介紹.在這裏只是簡單地說明CALL指令的R[A]表示的是即將要調用的函數,而B和C則分別表示參數個數加1,和返回值個數加1.
之所以這裏需要加1,其原因是:B和C使用零來表示變長的參數和變長的返回值,而實際參數個數就向後推了一個.
指令的介紹就先到此爲止了,其它的指令的實現也比較類似.