Lua 代碼是如何跑起來的

上一篇「C 代碼是如何跑起來的」中,咱們瞭解了 C 語言這種高級語言是怎麼運行起來的。程序員

C 語言雖然也是高級語言,可是畢竟是很 「古老」 的語言了(快 50 歲了)。相比較而言,C 語言的抽象層次並不算高,從 C 語言的表達能力裏,仍是能夠體會到硬件的影子。segmentfault

旁白:一般而言,抽象層次越高,意味着程序員的在編寫代碼的時候,心智負擔就越小。

今天咱們來看下 Lua 這門相對小衆的語言,是如何跑起來的。數組

解釋型

不一樣於 C 代碼,編譯器將其直接編譯爲物理 CPU 能夠執行的機器指令,CPU 執行這些機器執行就行。函數

Lua 代碼則須要分爲兩個階段:優化

  1. 先編譯爲字節碼
  2. Lua 虛擬機解釋執行這些字節碼
旁白:雖然咱們也能夠直接把 Lua 源碼做爲輸入,直接獲得執行輸出結果,可是實際上內部仍是會分別執行這兩個階段

字節碼

「CPU 提供了什麼」 中,咱們介紹了物理 CPU 的兩大基礎能力:提供一系列寄存器,能執行約定的指令集。編碼

那麼相似的,Lua 虛擬機,也一樣提供這兩大基礎能力:lua

  1. 虛擬寄存器
  2. 執行字節碼
旁白: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

  1. 不像 CPU 提供的物理集羣器,有不一樣的名字,字節碼的虛擬寄存器,是沒有名字的,只有數字編號。邏輯上而言,每一個函數有獨立的寄存器,都是從序號 0 開始的(實際上會有部分的重疊複用)
  2. Lua 字節碼,也提供了定義函數,執行函數的能力
  3. 以上的輸出結果是方便人類閱讀的格式,實際上字節碼是以很是緊湊的二進制來編碼的(每一個字節碼,定長 32 比特)

執行字節碼

Lua 虛擬機

Lua 虛擬機是一個由 C 語言實現的程序,輸入是 Lua 字節碼,輸出是執行這些字節碼的結果。內存

對於字節碼中的一些抽象,則是在 Lua 虛擬機中來具體實現的,好比:

  1. 虛擬寄存器
  2. 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 寄存器。

函數

幾個基本點:

  1. Lua 函數,能夠簡單的理解爲一堆字節碼的集合。
  2. Lua 虛擬機裏,也有棧幀的,每一個棧幀實際就是一個 C struct 描述的內存結構體。

執行一個 Lua 函數,也就是執行其對應的字節碼。

總結

Lua 這種帶虛擬機的語言,邏輯上跟物理 CPU 是很相似的。生成字節碼,而後由虛擬機來具體執行字節碼。

只是多了一層抽象虛擬,字節碼解釋執行的效率,是比不過機器指令的。

物理內存的讀寫速度,比物理寄存器要慢幾倍甚至幾百倍(取決因而否命中 CPU cache)。
因此 Lua 的虛擬寄存器讀寫,也是比真實寄存器讀寫要慢不少的。

不過在 Lua 語言的另外一個實現 LuaJIT 中,這種抽象仍是有很大機會來優化的,核心思路跟咱們以前在 「C 代碼是如何跑起來的」 中看到的 gcc 的編譯優化同樣,儘可能多的使用寄存器,減小物理內存的讀寫。

關於 LuaJIT 確實有不少很牛的地方,之後咱們再分享。

相關文章
相關標籤/搜索