Lua 和 Python 同為虛擬機器解釋型指令碼語言,為什麼 Lua 的執行速度比 Python 高?
回複內容:
前面幾位已經說的很好,我來做一下補充。
@馮東 和 @龐巍偉 都提到了Lua使用的是register-based的虛擬機器設計,我看到下面有人評論說既然這種VM的設計效能高,那麼為什麼Python和java還是使用的stack-based的設計。
我的理解是實現難度吧,register-based的設計中,一個操作需要關注到指令的運算元到底存放在哪裡,而stack-based的不需要,它分開了幾條指令,首先載入資料到棧頂,然後再進行操作,操作時預設的認為資料就存在棧頂了。(如果不清楚這個過程,可以拖上去看看 @龐巍偉 的回答,就不在這裡列出來了)
簡單的說,register-based的指令格式設計把stack-based的指令中分幾條指令要完成的事情用一條指令搞定了,快當然是快了,難度也加大了。
另外還有一點上面的回答中似乎沒有提到,Lua使用的是一遍遍曆就生產指令的方式,學過編譯原理的,大概都能知道一般分兩遍遍曆,第一遍產生AST,再一遍遍曆AST產生指令,而在Lua中是直接跳過了AST指令這一步的。
還是那句話,快是快了,代碼的實現難度也大了些。最早的Lua解譯器,也是使用lex、yacc這樣的工具來自動產生代碼的,後來為了提升效能,作者改成了自己手寫的遞迴下降的分析器。這部分代碼是我認為Lua代碼中最難理解的一個部分了--因為它要一遍分析幹太多的事情了。
我在閱讀Lua代碼的過程中,能充分感受到作者為了Lua在效能上的提升花費的心血,致敬。有一些 PUC-Rio Lua(也就是沒 JIT 的)和 Python 的 benchmark 對比。結論是 Python 比 C 大約慢 70 倍,Lua 大約慢 30-40 倍。
Lua 是 register-based VM。所謂的寄存器,其實並不神秘,就是 runtime stack 的 topmost frame [1] 是可以被 VM 指令隨機訪問的。至於為什麼 CPU 裡的某種硬體也叫寄存器,原因在這裡有解釋:《什麼是寄存器
》。
Stack frame 可以被隨機訪問之後,在同一個 VM 指令裡就可以用 native code 一次做很多事情。
可以看雲風的這篇 blog:《雲風的 BLOG: 虛擬機器之比較,lua 5 的實現
》
- Stack frame 就是 stack 中屬於同一個 function invocation 的所有 stack entries。
首先lua的虛擬機器非常簡單,指令設計也很精簡.
最關鍵的是, lua 是基於寄存器的虛擬機器實現,而python還有很多其他指令碼語言是基於堆棧的,基於寄存器的虛擬機器位元組碼更簡單,更高效,因為register based vm的位元組碼,一般同時包含了指令/運算元/操作目標等.
對比簡單的加法操作:
stack based 產生的位元組碼大概是這樣(僅僅是類比,不代表實際)
PUSH 1
PUSH 2
ADD // ADD 的操作結果存放eax
PUSH eax // 將結果push入堆棧,以便後面的代碼不會覆蓋eax
而register based 產生的位元組碼大概是這樣:
ADD 1,2,R1
就一行,R1存放1+2的結果
就這麼簡答的操作就已經相差4條指令,所以基於寄存器的虛擬機器位元組碼運行更有效率.python的一些設計特性,例如完全物件導向,同時也是它在效能表現上的負擔。
舉個很簡單的例子:
def test(): a = 1 b = 2 return a + b
Lua的語言特性設計的很緊湊,在各方面進行最佳化的困難路徑都比JS和Python少(得多),Python基於純對象的, 任意東西都是對象. 所以數值運算時, 還要進行轉換
lua 最新的5.3 已經支援整數類型, 加上基於寄存器的VM和優秀的編譯器, 想慢都難拋個磚,引個玉。
1. 基於棧和基於寄存器的不同是主要的效能差異原因。這點大家也都解析的非常清楚了,也很好想象。基於棧的求值過程必須使用棧頂的值,想想也知道是反人類的(哦不,反機器的-_-!)。因此會出現很多的push(load)和pop(store)指令,而基於寄存器的指令就一條完事了。但這都是在解釋執行的情況下,如果編譯到本地指令之後,理論上來說,基於寄存器還是基於棧的實現並沒有太多影響,因為都轉換成了硬體寄存器,兩者的轉換過程的開銷也沒有太多差別。
2. 為什麼採用基於棧的虛擬機器,除了實現簡單(後序遍曆AST就有了)之外,佔用空間小也是一個基於棧的虛擬機器的特點,便於網路傳輸和嵌入式裝置。Java在設計之初就是考慮到網路方面的應用,比如Applet技術,以及嵌入式裝置的運用。
3. 個人還有一個想法,不知道是否靠譜@RednaxelaFX。基於棧的虛擬機器的指令更加完整地保留了原始碼的求值過程,幾乎是AST直接『壓平』的結果,甚至很容易逆回原始碼。這就意味著基於棧的指令在後續操作中可以很容易轉換成需要的形式,以便於在不同的形式上做最佳化。棧代碼轉換成寄存器代碼沒有什麼效率影響,而寄存器代碼轉換成棧代碼就會出現比遍曆AST產生還要多的push(load)和pop(store)指令。棧代碼的產生適合直接從AST後續遍曆得到,因為求值的過程都是圍繞著棧頂。簡而言之,棧代碼是一個可塑性比較強的代碼,先存著,後面想怎麼處理都保留了可能性。
4. 文法分析到代碼產生過程減少pass數,個人覺得並沒有太大的意義。嚴格來講這個過程的效率應該不能算是performance的效率,最多隻是加快了從源碼的啟動時間。Performance應該從解釋執行開始比較。有的時候單趟編譯造成了複雜性反而得不償失,AST這樣的資料結構就適合在上面幹該乾的事。
5. 實際中虛擬機器的效率還和很多其他啊因素有關。比如很重要的方面就是記憶體回收。
6. 至於上升到指令集設計高度的話,不太瞭解,請R大來。 @RednaxelaFX。他應該會貼個這個傳送門虛擬機器隨談(一):解譯器,樹遍曆解譯器,基於棧與基於寄存器,大雜燴
Lua的指令集非常非常非常簡單,我對著指令說明看了半個小時就能看懂lua的彙編代碼了,再花十來分鐘就能手動修改lua二進位代碼了。而我甚至沒完整看過lua的源碼。占坑以我的觀點,最大的關鍵是在 lua 在語言層面相比 python 簡單了很多,所以他們的實現相應的就有了速度的差別。