上一篇「C 代碼是如何跑起來的」中,咱們瞭解了 C 語言這種高級語言是怎麼運行起來的。程序員
C 語言雖然也是高級語言,可是畢竟是很 「古老」 的語言了(快 50 歲了)。相比較而言,C 語言的抽象層次並不算高,從 C 語言的表達能力裏,仍是能夠體會到硬件的影子。segmentfault
旁白:一般而言,抽象層次越高,意味着程序員的在編寫代碼的時候,心智負擔就越小。
今天咱們來看下 Lua 這門相對小衆的語言,是如何跑起來的。數組
不一樣於 C 代碼,編譯器將其直接編譯爲物理 CPU 能夠執行的機器指令,CPU 執行這些機器執行就行。函數
Lua 代碼則須要分爲兩個階段:優化
旁白:雖然咱們也能夠直接把 Lua 源碼做爲輸入,直接獲得執行輸出結果,可是實際上內部仍是會分別執行這兩個階段
在「CPU 提供了什麼」 中,咱們介紹了物理 CPU 的兩大基礎能力:提供一系列寄存器,能執行約定的指令集。編碼
那麼相似的,Lua 虛擬機,也一樣提供這兩大基礎能力:lua
旁白:Lua 寄存器式虛擬機,會提供虛擬的寄存器,市面上更多的虛擬機是棧式的,沒有提供虛擬寄存器,可是會對應的操做數棧。
咱們來用以下一段 Lua 代碼(是的,邏輯跟上一篇中的 C 代碼同樣),看看對應的字節碼。用 Lua 5.1.5 中的 luac
編譯能夠獲得以下結果:指針
$ ./luac -l simple.lua main <simple.lua:0,0> (12 instructions, 48 bytes at 0x56150cb5a860) 0+ params, 7 slots, 0 upvalues, 4 locals, 4 constants, 1 function 1 [4] CLOSURE 0 0 ; 0x56150cb5aac0 2 [6] LOADK 1 -1 ; 1 # 將常量區中 -1 位置的值(1) 加載到寄存器 1 中 3 [7] LOADK 2 -2 ; 2 # 將常量區中 -2 位置的值(2) 加載到寄存器 1 中 4 [8] MOVE 3 0 # 將寄存器 0 的值,挪到寄存器 3 5 [8] MOVE 4 1 6 [8] MOVE 5 2 7 [8] CALL 3 3 2 # 調用寄存器 3 的函數,寄存器 4,和寄存器 5 做爲兩個函數參數,返回值放入寄存器 3 中 8 [10] GETGLOBAL 4 -3 ; print 9 [10] LOADK 5 -4 ; "a + b = " 10 [10] MOVE 6 3 11 [10] CALL 4 3 1 12 [10] RETURN 0 1 function <simple.lua:2,4> (3 instructions, 12 bytes at 0x56150cb5aac0) 2 params, 3 slots, 0 upvalues, 2 locals, 0 constants, 0 functions 1 [3] ADD 2 0 1 # 將寄存器 0 和 寄存器 1 的數相加,結果放入寄存器 2 中 2 [3] RETURN 2 2 # 將寄存器 2 中的值,做爲返回值 3 [4] RETURN 0 1
稍微解釋一下:code
0
開始的(實際上會有部分的重疊複用)Lua 虛擬機是一個由 C 語言實現的程序,輸入是 Lua 字節碼,輸出是執行這些字節碼的結果。內存
對於字節碼中的一些抽象,則是在 Lua 虛擬機中來具體實現的,好比:
table
等對於字節碼中用到的虛擬寄存器,Lua 虛擬機是用一段連續的物理內存來模擬。
具體來講:
由於 Lua 變量,在 Lua 虛擬機內部,都是經過 TValue
結構體來存儲的,因此實際上虛擬寄存器,就是一個 TValue
數組。
例以下面的 MOVE
指令:
MOVE 3 0
其實是完成一個 TValue
的賦值,這是 Lua 5.1.5 中對應的 C 代碼:
#define setobj(L,obj1,obj2) \ { const TValue *o2=(obj2); TValue *o1=(obj1); \ o1->value = o2->value; o1->tt=o2->tt; \ checkliveness(G(L),o1); }
其對應的關鍵機器指令以下:(主要是經過 mov
機器指令來完成內存的讀寫)
0x00005555555686f1 <+1889>: mov rdx,QWORD PTR [rax] 0x00005555555686f4 <+1892>: mov r14,r12 0x00005555555686f7 <+1895>: mov QWORD PTR [r9],rdx 0x00005555555686fa <+1898>: mov eax,DWORD PTR [rax+0x8] 0x00005555555686fd <+1901>: mov DWORD PTR [r9+0x8],eax
Lua 虛擬機的實現中,有這樣一個 for (;;)
無限循環(在 luaV_execute
函數中)。
其核心工做跟物理 CPU 相似,讀取 pc
地址的字節碼(同時 pc
地址 +1
),解析操做指令,而後根據操做指令,以及對應的操做數,執行字節碼。
例如上面咱們解釋過的 MOVE
字節碼指令,也就是在這個循環中執行的。其餘的字節碼指令,也是相似的套路來完成執行的。
pc
指針也只是一個 Lua 虛擬機位置的內存地址,並非物理 CPU 中的 pc
寄存器。
幾個基本點:
執行一個 Lua 函數,也就是執行其對應的字節碼。
Lua 這種帶虛擬機的語言,邏輯上跟物理 CPU 是很相似的。生成字節碼,而後由虛擬機來具體執行字節碼。
只是多了一層抽象虛擬,字節碼解釋執行的效率,是比不過機器指令的。
物理內存的讀寫速度,比物理寄存器要慢幾倍甚至幾百倍(取決因而否命中 CPU cache)。
因此 Lua 的虛擬寄存器讀寫,也是比真實寄存器讀寫要慢不少的。
不過在 Lua 語言的另外一個實現 LuaJIT 中,這種抽象仍是有很大機會來優化的,核心思路跟咱們以前在 「C 代碼是如何跑起來的」 中看到的 gcc
的編譯優化同樣,儘可能多的使用寄存器,減小物理內存的讀寫。
關於 LuaJIT 確實有不少很牛的地方,之後咱們再分享。