本文將介紹編程語言處理器的核心部分——虛擬機(Virtual Machine,VM)的實現。運行源代碼編譯結果的是運行時,運行時有多種實現方法,本文要講的虛擬機就是其中之一。
用軟件實現的 CPU 來運行
虛擬機這個單詞有多種不同的含義,本文中指“用軟件實現的(無實際硬件的)計算機”。
這與在虛擬機軟件和雲計算等語境中出現的虛擬機的含義不同。在虛擬機軟件等語境中,虛擬機是指通過把實際存在的硬件用某種軟件封裝進行虛擬化,從而實現多個系統的同時運行以及系統在硬件間的遷移。維基百科中把這種虛擬機歸類到了“系統虛擬機”中,而把本文所要介紹的虛擬機歸類到了“進程虛擬機”中。
Ruby 到版本 1.8 為止都沒有實現(進程)虛擬機,而是通過遍歷編譯器生成的語法樹(支持用指針鏈接起來的結構體所實現的 Ruby 程序語法的樹結構)來運行程序的(圖 1-12)。這種方法雖然非常簡單,但每執行一個指令都要訪問指針,成本不容小覷。在 Ruby 1.8 出來之前大家都說 Ruby 很慢,這就是其中一個原因。
int
vm(node* node) {
while(node) {
switch (node->type) {
case NODE_ASSIGN:
/* 賦值處理 */
...
break;
case NODE_CALL:
/* 方法調用處理 */
...
break;
...
}
/* 跳到下一個節點 */
node = node->next; /* ← 這裡慢 */
}
}
圖 1-12 語法樹解釋器(概要)
為什麼以前的 Ruby 很慢
我覺得需要說明一下為什麼這麼簡單的結構運行速度會那麼慢。大家都知道硬盤的訪問速度要比內存的訪問速度慢很多,可內存的訪問速度又如何呢?大家平常寫代碼時,很少會注意內存的速度吧。
但實際上,CPU 與內存之間的距離出乎意料地遠。與 CPU 的執行速度相比,通過內存總線讀取指定地址的數據的速度要慢很多。在訪問內存時,CPU 只能等待數據的到來,這個等待時間就會對執行速度產生影響。
為了削減這樣的等待時間,CPU 中內置了“內存緩存”(memory cache)的機制,該機制簡稱為“緩存”。緩存是 CPU 電路中嵌入的小容量的高速內存。通過事先將數據從主存讀取到緩存中,把對內存的讀寫轉化為對高速緩存的讀寫,能夠削減訪問內存的等待時間,提高處理速度。
由於緩存必須嵌入到 CPU 內部,所以其容量有著嚴格的限制,能夠預先讀入的數據很少。(現在的 CPU 都把緩存分為多個層級來增大緩存容量。即便如此,容量還是比主存小得多,而且也沒有解決難以事先將接下來要訪問的內存空間讀入到緩存的問題。)
為了有效利用緩存,需要把接下來要訪問的內存空間事先讀取到緩存中,但這是非常困難的。一般來說,只有在形成內存訪問局部性時才可能做到。也就是說,由於程序一次性訪問的內存空間非常小且距離非常近,所以會對一次性讀取到緩存的內存空間進行多次讀寫。
在虛擬機上靈活運用緩存
遺憾的是,從緩存訪問的立場來看,圖 1-12 那樣的語法樹解釋器是最糟糕的。構成語法樹的節點都是一個個單獨的結構體,各自的地址不一定鄰近,也不會連續。這就導致難以事先將接下來要訪問的內存空間讀入到緩存中。
這裡如果將語法樹轉換為指令序列,並儲存到連續的內存空間上,那麼內存訪問局部性就會有所增強,性能也會因為緩存的作用而得到極大的提升。
Ruby 1.9 中引入的被稱為 YARV 的虛擬機就使用這樣的方法實現了性能提升。YARV 是 Yet Another Ruby VM(另一個 Ruby 虛擬機)的縮寫。之所以叫這個名字,是因為當初開發時已經有多個以運行 Ruby 為目的的虛擬機在開發了。起初,YARV 只是一個實驗項目,但在這些虛擬機中只有它達到了能運行 Ruby 語言全部特性的效果,因此最終 YARV 替代了 Ruby 自己的虛擬機。
虛擬機的優點和缺點
採用虛擬機的語言中最有名的應該是 Java 了吧,但虛擬機這項技術並不是在 Java 中首次出現的,而是在 20 世紀 60 年代後期就已經有了。比如,20 世紀 70 年代初出現的 Smalltalk 語言就因從早期就採用了字節碼而名聲大噪(這只是部分原因)。再往前說,後來設計了 Pascal 語言的尼古拉斯·沃斯(Niklaus Wirth)以 Algol68 語言為基礎設計的 Eular 語言據說也完成了虛擬機的實現。Smalltalk 之父艾倫·凱(Alan Kay)說,Smalltalk 的虛擬機的實現受到了 Eular 的虛擬機的啟發。
說起 Pascal 就會想起 UCSD Pascal。由加州大學聖地亞哥分校開發的 UCSD Pascal 把 Pascal 程序變更為字節碼 P-code 之後運行。將 Pascal 程序變更為 P-code,可以輕鬆地將 UCSD Pascal 移植到各種操作系統和 CPU 的計算機上,這也使得 UCSD Pascal 作為具有較強移植性的編譯器被廣泛使用。
從這裡我們就能明白,虛擬機最大的優點就是擁有可移植性。配合各種各樣的 CPU 生成機器語言的代碼生成處理是編譯器中最複雜的部分。根據後續出現的各種 CPU 重新開發代碼生成處理,對語言處理器的開發者來說是很大的負擔。
現在 x86 和 ARM 等架構佔據統治地位,CPU 的種類比以往減少了許多,但在 20 世紀六七十年代,新架構層出不窮,甚至同一家公司的同一系列的計算機也會根據型號而使用不同的 CPU。虛擬機在減少這類負擔上起到了很大作用。
另外,虛擬機能夠配合目標語言進行設計,因此我們就可以將指令集的範圍限定在實現這個語言所必需的指令中。與通用 CPU 相比,可以縮小規格,開發也變得更簡單。
但虛擬機並非只有優點。與在硬件上直接執行相比,模擬虛擬的 CPU 運行的虛擬機在性能上有很大的問題。採用了虛擬機的語言處理器會產生幾倍,甚至幾百倍的性能損失。不過我們可以使用 JIT 編譯等技術在一定程度上減少這種性能損失。
虛擬機的實現技術
用硬件實現的真正的 CPU 與用軟件實現的虛擬機在性能上各有不同。下面我們來看一下虛擬機性能相關的實現技術,以下是具有代表性的幾種。
(1) RISC 與 CISC
(2) 棧與寄存器
(3) 指令格式
(4) 直接跳轉
RISC是Reduced Instruction Set Computer(精簡指令集計算機)的縮寫,是通過減少指令的種類、簡化電路來提高 CPU 性能的架構。在 20 世紀 80 年代流行的架構中,具有代表性的 CPU 有 MIPS 和 SPARC 等。在移動設備上廣泛使用的 ARM 處理器就屬於 RISC。
CISC 是與 RISC 相對的一個詞彙,是 Complex Instruction Set Computer(複雜指令集計算機)的縮寫,簡單來說就是“不是 RISC 的 CPU”。CISC 的每個指令執行的處理都非常大,而且指令的種類繁多,因此實現起來也比較複雜。
不過,RISC 與 CISC 的對立是 21 世紀之前的事情了,在如今的硬件 CPU 中,RISC 與 CISC 的對立沒有任何意義。這是因為純粹的 RISC 的 CPU 失去了人氣,現在已經很少見到了。即便如此,SPARC 還是存活了下來,被日本超級計算機“京”等設備採用。
RISC 中前景較好的 ARM 也在不斷增加指令,朝著 CISC 的方向發展。而作為 CISC 代表架構的英特爾 x86,通過在表面上提供複雜的指令集以維持與過去版本的兼容性,並在內部把指令轉換為類 RISC 的內部指令(μ op),從而實現了高速運行。
但對虛擬機來說,RISC 和 CISC 之爭有不同的意義。如果是用軟件實現的虛擬機,我們就不能忽視取指令(Instruction Fetch,IF)處理所需要的成本。也就是說,做同樣的處理時所需的指令數越少越好。好的虛擬機指令集是類 CISC 架構的指令集,它的全部指令都是高粒度的。
虛擬機的指令要儘可能地抽象,程序設計得小一些會比較好。有些虛擬機以緊湊化為目標,提供複合指令,把頻繁被連續調用的多條指令整合為一條,這樣的技術稱為“指令融合”或“super operator”。
棧與寄存器
虛擬機架構的兩大流派是棧式虛擬機和寄存器式虛擬機。棧式虛擬機原則上通過棧對數據進行操作(圖 1-13),而寄存器式虛擬機的指令中包含寄存器編號,原則上對寄存器進行操作(圖 1-14)。
push 1 ← ① 向棧push 1
push 2 ← ② 向棧push 2
add ← ③ 將棧中的兩個數相加,然後將結果push到棧中
執行各指令時棧的狀態

圖 1-13 棧式虛擬機的指令及其結構
load R1 1 ← ① 將第1個寄存器賦值為1
load R2 2 ← ② 將第2個寄存器賦值為2
add R1 R1 R2 ← ③ 將第1個寄存器和第2個寄存器的數值相加,並將結果保存到第1個寄存器
圖 1-14 寄存器式虛擬機的指令
與寄存器式虛擬機相比,棧式虛擬機更為簡單,程序也相對較小。然而,由於所有的指令都通過棧來交換數據,所以對指令之間的先後順序有很大的依賴,很難實施交換指令順序這樣的優化。
而寄存器式虛擬機由於指令中包含寄存器信息,所以程序相對較大。這裡需要注意的是,程序大小與取指令處理的成本不一定相關,這一點我們在後面也會提到。另外,寄存器式虛擬機由於顯式指定了寄存器,所以對指令順序依賴較小,優化空間較大。不過,小規模語言高度優化的例子幾乎不存在,所以這一點也就沒那麼重要了。
那麼棧式虛擬機和寄存器式虛擬機哪個更好呢?這個問題現在還沒有定論,使用這兩種架構的虛擬機都有很多。表 1-2 展示了這兩種架構在各種語言的虛擬機中的使用情況。我們發現,即使是同一語言,也會因實現的不同而採用不同的架構,有時採用棧式虛擬機,有時採用寄存器式虛擬機。這種現象很有趣。

表 1-2 各種語言的虛擬機架構
……
本書由Ruby 之父松本行弘在《日經Linux》雜誌上的連載整合而成,主要介紹了新語言Streem 的設計與實現過程。作者從設計Streem 這門新語言的動機開始講起,由淺入深,詳細介紹了新語言開發中的各個環節,以及語言設計上的糾結與取捨,其中也不乏對其他編程語言的調查與思考,向讀者展示了創建編程語言的樂趣。
閱讀更多 人民郵電出版社 的文章