詳解關於Lua 5.0實現原理學習筆記

來源:互聯網
上載者:User

Lua 5.0實現原理學習筆記是本文要介紹的內容,Lua 作為小型軟體的開發語言誕生在一個科學實驗室中。現在被世界上許多大型項目所採用,尤其廣泛的應用於遊戲開發中。我們是怎麼看待Lua的廣泛應用呢?我相信答案在於我們的設計和實現目標:提供一個簡單、高效、可移植、輕量級的嵌入式指令碼語言。從1993年Lua誕生之日起,它就成為了我們的主要目標。這些特點就是Lua能被業界廣泛採用的原因。

廣泛的使用必然會對對語言的特性提出許多新的要求。一些Lua新特性是應開發需要和使用反饋而產生。比如:corotine, garbage collection。這些特點對於遊戲開發很重要。

本文中,我們著重討論Lua 5.0相對於Lua 4.0的實現中一些主要新特點。

 
  1. Register-based virtual machine(基於寄存器的虛擬機器): 

通常,大部分的虛擬機器實現傾向於“基於堆棧的執行方式”。最早的有Pascal's P-machine,到今天的Java's JVM和Microsoft .Net環境。不過,目前基於寄存器的執行方式越來越受到關注,比如:Perl6(Parrot)的虛擬機器就計劃採用基於寄存器的方式。據我們了 解,Lua 5.0是第一個被廣泛應用的採用基於寄存器執行的虛擬機器實現。虛擬機器將在第七節討論。

New algorithm for optimizing tables used as arrays: 

不想其他指令碼語言,Lua不提供數群組類型。Lua開發人員通常會使用整數索引的Table來實現數組。Lua5.0中使用了一個新的演算法,檢測table 是否被用作一個數組,如果是的,那麼將把table中的值儲存在一個真正的數組中,而不是將他們加入雜湊表。該演算法將在第四節討論。

Closures的實現:

Lua 5.0 支援詞法範圍的first-class functions。使用基於數組的堆棧儲存當前活動的資料的機制來實現了一個著名的語言痛點。Lua將本地變數儲存在一個array-based stack中。當發生內嵌函數調用時,而且超出了當前函數的執行範圍的時候,這些儲存於array-based stack中的本地變數將被複製到堆中。Closures的實現將在第五節討論。

The addition of coroutines:

Lua 5.0語言中介紹了coroutines。儘管conroutine的實現和以前版本沒有什麼重大差別。為了保持本文的完整性。我們將在第六節對其進行討論。

剩 餘幾個章節主要是對這些討論的一些補充和背景知識介紹。在第二節中,我們介紹了Lua的設計目標以及這些目標xxxxx。在第三節中,我們描述了Lua是 怎麼表示“值”。儘管這些介紹的內容並不是Lua的最新特性,但是我們需要這些來為其他章節做補充。最後我們在第八節展示了一個簡單效能測試例子以及得出 一些結論。

1、Lua的設計和實現概述

就像在介紹中提到的那樣,Lua的設計目標:

簡單:我們採用最簡單的語言並且最簡單的C代碼來實現這個語言。這意味著一個簡單的文法和少量的語言結構,xxxxx。

高效:我們追求最快的編譯和最快的執行。這意味著一個快速、智能、一次掃描的編譯器和一個快速的虛擬機器。

可 移植:我們希望Lua運行在儘可能多的平台上。我們做到Lua的核心不需要任何修改就能夠在不同平台上編譯,Lua的程式不需要任何修改就能在不同平台上 的Lua解譯器中執行。這意味著一個乾淨的ANCI C實現以及周密考慮了移植問題。比如避免使用C語言的非標準文法和非標準庫。而且保證能夠在C++編譯器中乾淨編譯。我們追求無警告編譯。

可嵌入:Lua是一個擴充語言,他的是為了能夠給大型程式提供指令碼功能而作。這個目標意味著現存的C API是很簡單和強大,但是需要依賴於大多數內建的C類型。

低嵌入代價:我們希望Lua能夠很容易的被加入到其他應用程式中而不會導致整個系統膨脹。這意味著緊湊的C代碼和精巧的Lua核心和擴充以library形式加入到現有應用中。

這 些目標在有些時候是互相衝突的。比如:Lua經常被用作一個資料描述語言 (Data Description Language),用於讀寫設定檔,有時就像一個大的資料庫大至幾兆的Lua程式也是很常 見)。這意味著我們需要一個快速的編譯器。另外一方面,我們希望Lua程式執行起來儘可能的快速。這意味著需要一個智能的編譯器。可以為虛擬機器產生最佳化的 代碼。於是,編譯器的實現就需要在這兩個需求中尋找一個平衡點。然而,編譯器不能太大,否則它將導致整個應用體積膨脹。當前的實現中編譯器的體積大約佔整 個核心的30%。對於記憶體有限的應用中比如:嵌入式系統),可以做到嵌入不帶編譯器Lua。Lua程式需要預先編譯成二進位檔案,在運行時由一個很小 的載入模組載入。

Lua包含一個手工編寫的詞法掃描器和一個手工編寫的遞迴下降文法分析器。在3.0版本以前,Lua使用yacc產生的文法分析器。然而手工編寫的分析器更小,更高效,更可移植,而且可以完全重入保證可以多線程並發執行)。而且可以提供更完整的錯誤資訊。

Lua 編譯器不產生其他編譯器常有的中介層代碼。他在解析程式的同時直接產生虛擬機器指令。儘管沒有中介層代碼,Lua編譯器仍然對代碼進行了最佳化。比如:他延遲 產生基本運算式變數、常量)的代碼。當他解析到這些運算式時,他不產生指令;但使用一個簡單的結構來表示。因此就很容易判斷一個指令的運算元是常量還是 本地變數。如果是,則可以直接將這些常量或變數產生到指令中,這樣避免了無必要的“值”複製參閱第三節)。

為了確保能夠在不同C編譯器和平台中移植,Lua放棄了一些解譯器中常用的技巧。比如:直接使用作業系統中的線程。取而代之的是一個標準的while-switch分支迴圈。儘管這些地方的c代碼比較複雜難看,但這一切都是為了保證可移植性。

我 們認為我們已經達到了我們設計的目標。Lua現在是一種可移植性非常高的語言:只要有ANSI C編譯器就能夠編譯,從嵌入式系統到大型主機。Lua是真正的輕量級:在Linux上,一個獨立解譯器,包含所有的標準庫,只有不到150K大小;關鍵核 心部分只有不到100K。Lua是高效的:第三方測試結果表明Lua在指令碼語言中速度最快。我們也認為Lua是一個簡單的語言,文法像Pascal,語義 類似Schema。

2、值的表示

Lua是一種動態類型的語言:類型附屬於“值”而不是變數。Lua有八種基本類 型:nil, boolean, number, string, table, function, userdata, thread。Nil是一個只有一個值的標記類型;Boolean的值只有true和false;Number是雙精確度浮點數,和C語言的double一 樣,但是可以在編譯Lua核心的時候使用float或者long一些遊戲終端和小型系統在硬體上缺少對double的支援);String是有長度說明 的位元組數組,因此也可以用於儲存任意的位元據。Table是associative arrays,可以由任何值來進行索引除了nil)以及可以存放任何值。

Function既可以是Lua函數也可以是根據Lua虛擬機器調用協議編寫的C 函數。Userdata本質上是指向使用者記憶體塊的指標。有兩種風格:重型,記憶體塊由Lua分配並通過記憶體回收機制釋放;輕型,記憶體塊的分配和釋放都是由C 代碼管理。最後,thread表示coroutine。所有類型的“值”都是first-clast value:可以儲存在全域變數、局部變數、表中;可以當作參數傳遞給函數,可以通過函數傳回值返回。

Lua中的值是tagged union。也就是pairs(t, v),t是一個integer標誌,表示了v的類型。而v是一個C語言中的union。Nil有一個單一的值,Boolean和number是用過 “unboxed”值來實現:v就是union中的值。這意味著union必須足夠大,可以放下一個double。String, table, function, thread, userdata是通過指標引用來表示:v就是一個指向這些結構的一個指標。這些結構共用一個通用的head,保留了用於記憶體回收的必要資訊。結構的其他 部分有別於各個類別。

 
  1. typedef struct  
  2. {  
  3. int t;  
  4. Value v;  
  5. } TObject;  
  6.  
  7. type union  
  8. {  
  9. GCObject *gc;  
  10. void *p;  
  11. lua_Number n;  
  12. int b;  
  13. }Value; 

圖 一展示了Lua的值的具體實現。TObject實現了tagged units (t, v)。Value就是實現value的union。Nil的值不需要顯示的表示出來,因為它的tag就足以標識了。欄位n用於表示number(預設情況 下,lua_Number就是double)。欄位b表示boolean。欄位p表示輕型userdata。欄位gc表示其他類型的值string, table, function, 重型userdata, thread),gc指向的內容包含了記憶體回收時所必需的資訊。

由於採用了 tagged union來表示“值”,導致複製資料的代價增大:在一個32位支援64位浮點的硬體上。TObject佔據12個位元組如果硬體讓double類型按8 位元組對齊,則需要16位元組)。那麼拷貝一個value需要複製3或4)個機器字長。這確實是一個問題,使用ANSI C很難找到一個更好的辦法。一些動態類型語言smalltalk80)使用每個指標的空餘的位來儲存類型資訊。這個在很多硬體上都可以實現。由於通常會 按照某個數值對齊4位元組,8位元組)。那麼指標的最後2個位始終是為零。那麼就可以用於其它用途。但是這種方法失去了可移植性,而且在ANSI C中也是做不到。標準C語言不保證指標類型和任何數實值型別匹配,也沒有針對指標的標準的“位”操作方法。

另外一個減少“值”的大小的觀 點:保持tag,但是不把double放入union。舉例來說,所有的成員都是在堆中分配的對象就像string)。Python就使用這種技 術,xxxx)然而這樣會導致效能急劇下降。可以做一些改進,一個整數可以表示成unboxed value並直接放入union中,而浮點數放到堆中。這樣做效能和空間都能夠解決,但是這樣導致實現起來非常複雜。

像早期的解釋型語 言,比如:Snobol和Icon,Lua用hash表來儲存string:儲存不同字串的唯一版本。字串是不可更改:一旦初始化,就不能被修改。字 符串的Hash值通過一些位操作和計算得出,Hash值在字串用於進行快速比較和作為table的索引之前計算出來並儲存。如果字串很長,Hash值 的計算並不會遍曆整個字串。提高長字串操作效能很重要,因為長字串在Lua程式中相當常見。比如,經常會把整個檔案讀入到記憶體中當作一個長字串來 儲存。

3、Tables

Table可以說是Lua中唯一的資料結構。Table無論是在語言中還是語言的實現中都是一個關鍵角色。從另一方面來說,由於沒有其他的資料結構機制,迫使Table的實現必需足夠高效和功能強大。

Figure 2

Table在Lua中是associative array。也就是他們可以被任何類型的值索引除了nil)以及可以存放任何類型的值。array的大小也會根據操作動態改變。

不 像其他指令碼語言,Lua不提供數群組類型。數組是由以整數作為索引的table來實現。用table來實現array給語言帶來了一些好處。第一,簡單性: 不需要分別對於table和array提供兩套不同的操作。而且程式員在開發時不需要在table和array之間進行選擇。因為只有table,沒有其 他選擇。稀疏數組在Lua中很容易實現。舉例來說:在Perl中,如果你執行下面的語句$a[1000000000]=1;那麼會導致“out of memory”錯誤。這條語句導致分配了十億個元素。
而在Lua程式中,a={[1000000000]=1},僅僅是建立了一個只有一個元素的表。

在Lua 4.0之前,table都是用hash表來實現:所有的索引值對都被完整的儲存起來。

對於數組來說a={10, 10, 10},其索引值對就是(1, 10), (2, 10), (3, 10)。

針 對把table當作數組使用這種情況,Lua 5.0 提供了一種新的最佳化演算法。當發現table是被當作數組來使用時,他並不儲存整數的索引,也就是上面的1, 2, 3。而是將這些值放入一個真正的數組中。這樣可以大大提高存取效率。更準確地說,在Lua 5.0中table被實現成一種混合結構。包含一個hash表部分和一個數組部分。圖 2詳細介紹了這個實現。從程式來看這些都是透明的。程式員無需關心他的資料究竟是存於hash表還是數組。Lua的內部會自動和動態對table中的數 據進行調整。那些鍵是從1到n的放入數組部分;那些鍵根本就不是整數或者是整數但超出數組大小範圍的放入hash表部分。

 
  1. xxxxxxxx 

混 合模式有兩個優勢。第一,當用整數索引來訪問時,由於不需要計算hash值,所以提高了效能。第二,由於不需要儲存索引值,節約了不少空間。也就是當 table被當作數組使用時,他內部會使用一個真正的數組,那麼索引值就不需要額外分配空間儲存。如果table僅當作數組時,他的hash部分是不存 在;如果僅當作associative array,他的數組部分將不存在。這個確保了記憶體空間沒有浪費。這點很重要,因為在Lua程式中通常會用到大量的小table。

 
  1. xxxxxxxxxxxx 

4、函數和閉包 Functions and Closures

當Lua編譯一個函數時,他產生了一個原型包含虛擬機器指令、常量、調試資訊。在啟動並執行時候,他建立一個閉包。閉包中儲存了以下內容:一個到他的原型的引用,一個到執行環境的引用,一個存放upvalues引用的數組。

一類函數要應用於詞法範圍導致了一個問題。就是如何訪問到外部函數的局部變數。請看圖3中的例子。當add2被調用時,他訪問的是他的外部函數的局部變數。然後在那個時候,add已經退出了。如果x是在堆棧上,由於函數的堆棧已經消失,那麼局部變數也就消失了。

大多數的過程類語言都採取各種辦法來避免類似問題:嚴格定義詞法範圍e.g., Python),不提供一類函數e.g., Pascal),或者兩者都採用e.g., C)。函數類語言不存在這樣的問題。xxxxxxxxxxx

Lua 使用了一個叫upvalue的結構來實現closure。任何外部的局部變數都可以通過upvalue間接訪問到。upvalue最初存放的是指向堆棧單 元的指標。當局部變數需要超出函數範圍的時候,變數本身將被移動到upvalue內部。由於訪問外部局部變數都是通過upvalue間接來完成,所以這種 移動對於程式來說是透明的。對於聲明這個局部變數的函數來說,他訪問的是他自己的局部變數,可以直接從堆棧中得到。

如果一個函數產生了多 個內部閉包,那麼這些閉包需要能夠正確的訪問相同的局部變數。為了實現這個需求,Lua對於每一個堆棧的所有upvalue維護了一個鏈表。當一個新的閉 包被建立出來,他將遍曆所有的外部局部變數。對於每一個局部變數,他都試圖在鏈表中尋找一個匹配的upvalue。如果能夠找到,則直接引用這個 upvalue;否則就建立一個新的upvalue並且加入鏈表。注意:鏈表的搜尋過程相當快捷,因為對於每一個局部變數只會建立一個upvalue節 點。一旦upvalue不再被引用,將自動由垃圾收集器負責回收。

在多層函數嵌套的情況下,內層函數仍然可以訪問到非直接外層函數所定義的局部變數。Lua通過flat closure來解決這個問題。xxxxxxxxxxxxxx

5、Threads and Coroutines

從 5.0起,Lua實現了非對稱協程也叫半對稱協程或半協程)。通過三個Lua庫函數來實現:create, resume, yield。create函數接受一個main函數值作為參數,然後建立一個協程來執行這個函數。返回一個thread值來表示這個協程。像其他類型一 樣,協程也是一類值)。resume函數啟動或繼續執行某個協程。yield函數掛起一個協程,並把執行權返回給調用resume的函數所在的那個協程。

理 論上,每一個協程擁有一個屬於他自己的堆棧。實際上,每個協程又兩個堆棧,但是我們視之為一個抽象的堆棧)。Lua中的協程是可堆疊的,也就是我們不管 在多深的函數嵌套中,我們都可以通過調用suspend來掛起當前協程。解譯器簡單的停用當前協程的整個堆棧,而啟用另外一個協程的堆棧。程式可以隨時啟 動一個掛起的協程。記憶體回收行程會回收那些不再被使用的協程的堆棧。

小結:詳解關於Lua 5.0實現原理學習筆記的內容介紹完了,希望通過本文的學習能對你有所協助!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.