Friday, March 15, 2013

探索Lua5.2內部實現:虛擬機指令(2) MOVE & LOAD


nameargsdesc
OP_MOVEABR(A) := R(B)
OP_MOVE用來將寄存器B中的值拷貝到寄存器A中。由於Lua是register based vm,大部分的指令都是直接對寄存器進行操作,而不需要對數據進行壓棧和彈棧,所以需要OP_MOVE指令的地方並不多。最直接的使用之處就是將一個local變量複製給另一個local變量時:
  1. local a;  
  2. local b = a;  
  1. 1 [1] LOADNIL 0 0  
  2. 2 [2] MOVE 1 0  
  3. 3 [2] RETURN 0 1  
在編譯過程中,Lua會將每個local變量都分配到一個指定的寄存器中。在運行期,lua使用local變量所對應的寄存器id來操作local變量,而local變量的名字除了提供debug信息外,沒有其他作用。
在這裡a被分配給register 0,b被分配給register 1。第二行的MOVE表示將a(register 0)的值賦給b(register 1)。其他使用的地方基本都是對寄存器的位置有特殊要求的地方,比如函數參數的傳遞等等。
nameargsdesc
OP_LOADKA BxR(A) := Kst(Bx)
LOADK將Bx表示的常量表中的常量值裝載到寄存器A中。很多其他指令,比如數學操作指令,其本身可以直接從常量表中索引操作數,所以可以不依賴於LOADK指令。
  1. local a=1;  
  2. local b="foo";  
  1. 1 [1] LOADK 0 -1 ; 1  
  2. 2 [2] LOADK 1 -2 ; "foo"  
  3. 3 [2] RETURN 0 1  
  4. onstants (2) for 0x80048eb0:  
  5. 1 1  
  6. 2 "foo"   
nameargsdesc
OP_LOADKXAR(A) := Kst(extra arg)
LOADKX是lua5.2新加入的指令。當需要生成LOADK指令時,如果需要索引的常量id超出了Bx所能表示的有效範圍,那麼就生成一個LOADKX指令,取代LOADK指令,並且接下來立即生成一個EXTRAARG指令,並用其Ax來存放這個id 。5.2的這個改動使得一個函數可以處理超過262143個常量。
nameargsdesc
OP_LOADBOOLABCR(A) := (Bool)B; if (C) pc++
LOADBOOL將B所表示的boolean值裝載到寄存器A中。B使用0和1分別代表false和true。C也表示一個boolean值,如果C為1,就跳過下一個指令。
  1. local a = true;  
  1. 1 [1] LOADBOOL 0 1 0  
  2. 2 [1] RETURN 0 1   
C在這裡的作用比較特殊。要了解C的具體用處,首先要知道lua中對於邏輯和關係表達式是如何處理的,比如:
  1. local a = 1 < 2  

對於上面的代碼,一般我們會認為lua應該先對1<2求出一個boolean值,然後放入到a中。然而實際上產生出來的代碼為:
  1. 1 [1] LT 1 -1 -2 ; 1 2  
  2. 2 [1] JMP 0 1 ; to 4  
  3. 3 [1] LOADBOOL 0 0 1  
  4. 4 [1] LOADBOOL 0 1 0  
  5. 5 [1] RETURN 0 1  
  6. onstants (2) for 0x80048eb0:  
  7. 1 1  
  8. 2 2   
可以看到,lua生成了LT和JMP指令,另外再加上兩個LOADBOOL對於a賦予不同的boolean值。LT(後面會詳細講解)指令本身並不產生一個boolean結果值,而是配合後面緊跟的JMP實現true和false的不同跳轉。如果LT評估為true,就繼續執行,也就是執行到JMP,然後調轉到4,對a賦予true;否則就跳過下一條指令到達第三行,對a賦予false,並且跳過下一個指令。所以上面的代碼實際的意思被轉化為:
  1. local a;  
  2. if 1 < 2 then  
  3.     a = true;  
  4. else  
  5.     a = false;  
  6. end  

邏輯或者關係表達式之所以被設計成這個樣子,主要是為if語句和循環語句所做的優化。不用將整個表達式估值成一個boolean值後再決定跳轉路徑,而是評估過程中就可以直接跳轉,節省了很多指令。
C的作用就是配合這種使用邏輯或關係表達式進行賦值的操作,他節省了後面必須跟的一個JMP指令。
nameargsdesc
OP_LOADNILABR(A), R(A+1), ..., R(A+B) := nil
LOADNIL將使用A到B所表示範圍的寄存器賦值成nil。用範圍表示寄存器主要為了對以下情況進行優化:
  1. local a,b,c;  
  1. 1 [1] LOADNIL 0 2  
  2. 2 [1] RETURN 0 1   
對於連續的local變量聲明,使用一條LOADNIL指令就可以完成,而不需要分別進行賦值。
對於一下情況
  1. local a;  
  2. local b = 0;  
  3. local c;  
  1. 1 [1] LOADNIL 0 0  
  2. 2 [2] LOADK 1 -1 ; 0  
  3. 3 [3] LOADNIL 2 0   
在Lua5.2中,a和c不能被合併成一個LOADNIL指令。所以以上寫法理論上會生成更多的指令,應該予以避免,而改寫成
  1. local a,c;  
  2. local b = 0;  

No comments: