棧式虛擬機和寄存器式虛擬機?
這兩者究竟有什麼大的區別?為什麼JVM是基於前者,Lua是後者呢?是因為作者當初自己按喜歡決定?還是另有原因?
前面有回答提到了俺的老文:虛擬機隨談(一):解釋器,樹遍歷解釋器,基於棧與基於寄存器,大雜燴
俺就王婆賣瓜自己也發一次吧。備註:本回答全文原創,未經許可不允許轉載。特別不允許清華大學出版社以任何形式轉載本文。此備註背景請參考:http://www.douban.com/doulist/42057979/
===========================================
太長不讀(TL;DR)版定性分析:
對於解釋器來說,解釋器開銷主要來自解釋器循環(fetch-decode/dispatch-execute循環)中的fetch與decode/dispatch,反而真正用於執行程序邏輯的execute部分並不是大頭。每條指令都要經歷一輪FDX循環。因而減少指令條數可以導致F與D的開銷減少,於是就提升了解釋器速度。
基於棧與基於寄存器的指令集,用在解釋器里,籠統說有以下對比:- 從源碼生成代碼的難度:基於棧 &< 基於寄存器,不過差別不是特別大
- 表示同樣程序邏輯的代碼大小(code size):基於棧 &< 基於寄存器
- 表示同樣程序邏輯的指令條數(instruction count):基於棧 &> 基於寄存器
- 簡易實現中數據移動次數(data movement count):基於棧 &> 基於寄存器;不過值得一提的是實現時通過棧頂緩存(top-of-stack caching)可以大幅降低基於棧的解釋器的數據移動開銷,可以讓這部分開銷跟基於寄存器的在同等水平。請參考另一個回答:寄存器分配問題? - RednaxelaFX 的回答
- 採用同等優化程度的解釋器速度:基於棧 &< 基於寄存器
- 交由同等優化程度的JIT編譯器編譯後生成的代碼速度:基於棧 === 基於寄存器
- 盡量實現簡單:選擇基於棧
- 傳輸代碼的大小盡量小:選擇基於棧
- 純解釋執行的解釋器的速度:選擇基於寄存器
- 帶有JIT編譯器的執行引擎的速度:隨便,兩者一樣;對簡易JIT編譯器而言基於棧的指令集可能反而更便於生成更快的代碼,而對比較優化的JIT編譯器而言輸入是基於棧還是基於寄存器都無所謂,經過parse之後就變得完全一樣了。
===========================================
JVM的選擇
JVM當初設計的時候非常重視代碼傳輸和存儲的開銷,因為假定的應用場景是諸如手持設備(PDA)、機頂盒之類的嵌入式應用,所以要代碼盡量小;外加基於棧的實現更簡單(無論是在源碼編譯器的一側還是在虛擬機的一側),而且主要設計者James Gosling的個人經驗上也對這種做法非常熟悉(例如他之前實現過PostScript的虛擬機,也是基於棧的指令集),所以就選擇了基於棧。
回頭看,這個決定也還算OK,可惜的是基於棧的設計並沒有讓Java的代碼傳輸大小減小多少。
這是因為:Java代碼是以Class文件為單位來傳輸與存儲的。Java從設計之初就非要支持分離編譯(separate compilation)與按需動態類載入(on-demand dynamic class loading),導致Java的Class文件必須獨立的(self-contained)——每個Class文件必須自己攜帶自己的常量池,其主要信息是字元串與若干其它常量的值,以及用於符號鏈接的符號引用信息(symbolic reference)。
如果大家關注過Class文件的內容的話,會知道其實通常Class文件里表示程序邏輯的代碼部分——「位元組碼」——只佔Class文件大小的小頭;而大頭都被常量池佔了。而且多個Class文件的常量池內容之間常常有重疊,所以當程序涉及多個Class文件時,就容易有冗餘信息,不利於減少傳輸/存儲代碼的大小。大家或許還記得Google在Google I/O 2008的Dalvik VM Internals演講里,Dan得意的介紹到Dalvik的Dex格式在未壓縮的情況下都比壓縮了的JAR文件還小么?(下面數據引用自演示稿第22頁)common system libraries
(U) 21445320 — 100% (J) 10662048 — 50% (D) 10311972 — 48%web browser app(U) 470312 — 100%
(J) 232065 — 49% (D) 209248 — 44% alarm clock app (U) 119200 — 100% (J) 61658 — 52% (D) 53020 — 44%(U) uncompressed jar file(J) compressed jar file
(D) uncompressed dex file
Dan準確的介紹了Dex體積更小的原因:一個Dex相當於一個或多個JAR包,裡面可以包含多個Class文件對應的內容。一個Dex文件里的所有Class都共享同一個常量池,因而不會像Class文件那樣在多個常量池之間有冗餘。這樣Dex文件就等同於在元數據層面上對JAR文件做了壓縮,所以前者比後者更小。
但是很明顯後來有不少群眾不明就裡,以為Dalvik的Dex文件更小是跟基於寄存器的選擇相關的——其實正好相反,在位元組碼部分,Dalvik的位元組碼其實比JVM的位元組碼更大。只要仔細讀了同一個演示稿就會看到(第37頁)The Register Machine
- 30% fewer instructions
- 35% fewer code units
- 35% more bytes in the instruction stream
- but we get to consume two at a time
而其實Java世界裡早就發現了這個問題,並且有一個傳輸/存儲格式跟Dex文件應用了類似的壓縮思路:pack200格式。它也是把JAR包里的所有Class文件的常量池匯總為一個,以此壓縮掉其中的冗餘。以pack200格式打包的Java應用就不會比用Dex格式打包大了。
之前在Oracle的HotSpot JVM組工作時,跟同事們討論一些與此相關的話題,大家的共識是如果JVM的指令集是在20年後的今天設計的話,很有可能也會跟現在的潮流相似,基於寄存器。不過就算保持現在這樣用基於棧的指令集也挺好。
目前Class文件/JAR文件的真正讓大家頭疼的問題並不是基於棧還是基於寄存器的設計,而是別的地方,例如:Class文件方面:- 各種人為的大小限制都跟不上時代了,例如每個方法的位元組碼最多65535位元組;
- 要生成StackMapTable太鬧心;
- 常量池的組織方式不便於直接從文件映射到內存然後高效執行;可以有更高效的組織方式。
JAR文件方面:
- 如前文提的,多個Class文件之間的常量池冗餘;
- 缺少帶有強語義的描述模塊的信息;
- 等等…
一切都有待未來版本的Java繼續改進。
===========================================
Lua的選擇
官方版Lua的設計可以從經典文章 The Implementation of Lua 5.0 一窺究竟。它提到:For ten years (since 1993, when Lua was first released), Lua used a stack-based virtual machine, in various incarnations. Since 2003, with the release of Lua 5.0, Lua uses a register-based virtual machine.
也就是說Lua 5.0之前的Lua其實是用基於棧的指令集,到5.0才改為用基於寄存器的。
接下來:Register-based code avoids several 「push」 and 「pop」 instructions that stack-based code needs to move values around the stack. Those instructions are particularly expensive in Lua, because they involve the copy of a tagged value, as discussed in Section 3. So, the register architecture both avoids excessive copying of values and reduces the total number of instructions per function. Davis et al. [6] argue in defense of register-based virtual machines and provide hard data on the improvement of Java bytecode. Some authors also defend register-based virtual machines based on their suitability for on-the-fly compilation (see [24], for instance).
所以Lua 5.0開始改為選用基於寄存器的指令集設計,主要是出於 (1) 減少數據移動次數,降低由數據移動帶來的拷貝開銷,和 (2) 減少虛擬指令條數 這兩點考慮。
從純解釋器的角度看,這兩點考慮是非常合理的。
不過如果官方版Lua有JIT編譯器的話,它就沒必要這麼做了——基於棧和基於寄存器的指令集只要經過合理的編譯,得到的結果會是一模一樣的。至於LuaJIT,LuaJIT 1.x的位元組碼設計源於Lua 5.1.x系列,因而也是基於寄存器的;LuaJIT 2.x系列的位元組碼雖然重新設計了,但應該還是受到之前設計的影響而繼續採用了基於寄存器的設計。像Mike Pall那種想榨乾一切性能的思路,即便有優化的JIT編譯器,也還是想讓解釋器盡量快的心情也是可以理解的。探索Lua5.2內部實現:虛擬機指令(1) 概述
RednaxelaFX 的回答很好,這裡補充一些。雖然不做虛擬機好久了,但是從目前跟蹤的論文和項目情況看,應該知識沒有太過時,畢竟hotspot以後虛擬機發展不多。
- 基於棧的代碼在hotspot虛擬機代碼優化時候會帶來一些麻煩,主要是引入不必要的數據依賴。這些依賴,hotspot運行時間優化比較難辨識,不像基於寄存器的代碼,數據依賴比較少,運行時間優化自由度大。
- 基於棧的實現也簡單,intel x86架構早期的數學處理器x87就是基於棧的,只操作棧頭的兩個數據。但是實際實現已經早就不用這種架構,如果你看編譯器生成的浮點代碼,也是更多用SSE指令來做浮點運算了。
自問自答一下先,發現了虛擬機隨談(一):解釋器,樹遍歷解釋器,基於棧與基於寄存器,大雜燴這個好文,我先慢慢看,若大家想回答,無任歡迎
推薦閱讀:
※計算機會認為(-b)是(0-b)還是((-1)*b)?
※怎麼從零學起成為一名黑客?
※寫代碼上癮是一種什麼樣的體驗?
※能推薦國外大學適合自學的CS課程(自帶源碼與資料)嗎?非coursera/edx
※真·二本學校的計算機類專業尖子生的專業能力可能超過985名校的優秀學生嗎?
TAG:Java虛擬機JVM | 虛擬機 | 計算機科學 | Lua | DalvikAndroid |