虛擬機二三事 - Part 0. 虛擬機的介紹
# 一、虛擬機是什麼 ?
# 1-1. 虛擬機的介紹
根據虛擬機的維基 (opens new window),虛擬機總共分為兩種
- System Virtual Machine - 完全硬體虛擬化的技術,提供整個作業系統所需要的功能,例如 : VMware 和 Virtual box
- Process Virtual Machine - 讓我們的程式可以順利執行在不同平台上,也是本篇的主角。
當然虛擬機不僅限在這兩種層級,再更仔細區分的畫分
Hardware Level
: VMWare, Virtual PC, Virtual boxOS Level
: Virtual Servers, BSD Jail, ZapProgramming Language Level
: JVM, .NETNetwork Level
: VLAN, VPN
另外,像 Qemu, Gameboy emulator 都算是虛擬機的一種。
我們在這裡探討的主要是行程虛擬機(Process Virtual Machine),在維基百科所述
Its purpose is to provide a platform-independent programming environment that abstracts away details of the underlying hardware or operating system and allows a program to execute in the same way on any platform. A process VM provides a high-level abstraction – that of a high-level programming language (compared to the low-level ISA abstraction of the system VM). Process VMs are implemented using an interpreter; performance comparable to compiled programming languages can be achieved by the use of just-in-time compilation。
Process Virtual Machine
,就像是一般的 Process 一樣啟動時創建,退出時銷毀。 它的目的是提供一個獨立於平台的程式環境,將底層硬體和作業系統的細節抽象化,並且允許程式在任何平台上都可以順利執行。 碼執行效率。
Process VM 可模擬執行單一 APP 所需抽象(Abstract)且與平台無關(Platform Independent)的執行環境,藉此提供程式的可攜性(Portability)與執行彈性( Flexibility),因為通常與程式語言有關,又稱語言虛擬機器(Language VM),通過直譯器(Interpreter)實現搭配 Just-in-time compiler ,Process VM 可以媲美編譯器的產生出的二進位檔案執行的效率。
簡單來說,如同陳鍾誠老師的投影片 (opens new window) ,Process VM 是一種虛擬碼的直譯器,如果我們將原始程式語言編譯成和機器架構無關的中間碼或虛擬碼,利用 Process VM 去直譯執行,再搭配及時編譯技術,可以高效率且跨平台的執行程式碼。
# 1-2. Stack-based VM vs Register-based VM
這裡我只做一些簡單的介紹,我會以 Part 1. 的文章深入做兩者的比較。
- Stack-based VM
Stack-based VM Wiki (opens new window) 這種類型的VM,記憶體以堆疊(Stack)儲存,適用於 0-operand instruction set,因為 memory fetch data 的動作都可以由 stack push / pop 來完成。執行運算時,由堆疊的頂端取出運算元,運算結束再存回堆疊的頂端。相較於累加器(採用 1-operand instruction set),和暫存器機( 2-operand instruction set 或 3-operand instruction set),用零位址指令( 0-operand instruction set)實作的堆疊機器,它的好處是程式碼密度(code density)相對較大,因此,它的程式通常較小。虛擬機例子如: JVM、.NET、php、python、Old Javascript engine
- Register-based VM
Register-based 適用於 2-operands 或 3-operands 的 instruction set,它不同於 Stack-based 的地方在於資料是從 registers fetch 而不是 stack,而每個 register 有它自己獨特的 address,這種性質讓 CPU 可以直接對 registers 存取資料,而不用藉由頻繁的 push/pop 動作來存取 stacks。虛擬機例子如: Lua、Dalvik、All modern Javascript engine
# 1-3. 參考連結
Stack-based or Register-based Virtual Machine (opens new window)
指令集架構 計算機也跟人類一樣,需要提供一套完整的語言讓人們跟它充分溝通,以完成正確的計算工作。 (opens new window)
# 二、虛擬機躲在哪裡 ?
虛擬機 is everywhere,在我們平常使用的系統,都藏有虛擬機。接下來舉幾個小例子。
# 2-1. Android Runtime
Android Runtime(縮寫為ART),是一種在Android作業系統上的執行環境,Android 使用虛擬機去執行 android 應用程式的 APK 文件,在 Andoird 2.2 引入 Dalvik虛擬機,在虛擬機執行的過程中,不斷利用連續的效能分析搭配及時編譯技術來最佳化程式碼的執行。
而後引進的 ART 可以把應用程式的 bytecode 直接轉換成 machine code,是 android 的新的虛擬機。ART引入了AOT這種預編譯技術,在應用程式安裝的過程中,ART就已經將所有的位元組碼重新編譯成了機器碼。因此在用 google play 下載應用程式時需要事先編譯,時間才會稍微久一點。應用程式執行過程中無需進行即時的編譯工作,只需要進行直接呼叫。因此,ART極大的提高了應用程式的執行效率,同時也減少了手機的電量消耗,提高了行動裝置的續航能力,在垃圾回收等機制上也有了較大的提升。
這裡強力推薦一篇文章 Android Runtime — How Dalvik and ART work? (opens new window),之後會花時間仔細介紹。
JVM 和 DVM 的差異
JVM 是 stack-based VM,意味著需要去堆疊中讀寫資料,所需要的指令會更多,頻繁的從堆疊 push/pop 這樣會導致速度變慢,對於性能有限的移動設備顯然不合適。
DVM是 stack-based VM,它沒有基於堆疊的虛擬機在拷貝資料時而使用大量的出入堆疊指令,同時指令更緊湊、簡潔。但是由於顯式的製定了操作數,所以基於寄存器的指令會比基於堆疊的指令要大。
Google Android 開發為了技術自主,迴避 Oracle 的商業爭議,開發出可以取代 JVM 的 DVM,DVM 可以執行 .class
的 byte code,也能像 JVM 一樣有很多效能上的最佳化。
# 2-2. 程式語言
現在大部分的程式語言,都是需要使用虛擬機才能執行。以下列出最有名氣的虛擬機們
JVM (Java Virtual Machine)
- Java, Kotlin, Scala, Groovy
- Run 在 JVM 程式語言清單 (opens new window)
CLR (Common Language Runtime)
- C#, F#, Visual Basic
- Run 在 CLR 程式語言清單 (opens new window)
CIL(Common Intermediate Language),是一個中間表示法。
Google V8 - Javascript
Pypy, Cython, Jython - Python
- Ruby, Lua....
歸類一下有使用虛擬機的程式語言特徵
- 先將語言轉成中間碼 (CLI, IR, Bytecode),再用直譯器執行
- 有用到及時編譯技術
- 有提供記憶體垃圾回收機制,寫程式碼時不會像C、C++、Rust 那樣赤裸裸面對記憶體的配置與刪除 (非必要,像 Golang 就沒有虛擬機)
所以我們現在大部分的程式語言都有用到 Virtual Machine, 原因是我們想要利用轉換成中間表示型達到跨平台的功用,如同 Java 的精神 Write Once, run everywhere。此外,虛擬機也有支援垃圾回收機制,在開發時可以大幅縮短除錯時間。
沒有虛擬機卻可以垃圾回收 ? 參考此篇問答 (opens new window)
其實虛擬機不一定要做垃圾回收,反之做垃圾回收也不一定要有虛擬機。垃圾回收就連 C / C++ 都有垃圾回收的程式 (A garbage collector for C and C++ (opens new window)),在 Go 語言編譯的時候,編譯器會放入一些關於垃圾回收的程式碼進去,去實現註冊、暫停、回收、檢查。因此可以說編譯好的 Go 語言程式已經包含了垃圾回收的程式進去。
而 go 語言不想要加入虛擬機的元素,可參考這篇問答 (opens new window),是因為 Go 設計理念就是希望越簡潔越好,捨棄一切非必要的東西。如果加入虛擬機的元素進去,不論在撰寫程式或是效能調校都會更加複雜。
# 2-3. BPF 和 eBPF
Berkeley Packet Filter (BPF) 最初的動機的確是封包過濾機制,但擴充為 eBPF (Extended BPF) 後,就變成 Linux 核心內建的內部行為分析工具包含以下:
- 動態追蹤 (dynamic tracing)
- 靜態追蹤 (static tracing)
- profiling events
形式上說來,bpf 就是 in-kernel virtual machine(register machine),BPF 可以藉由 BPF 虛擬機來實現封包的過濾功能,並且搭配及時編譯技術,大幅提升指令執行效能。
這裡只是做簡單介紹,eBPF 虛擬機以後會花額外的篇幅來介紹。
# 2-4. 你不要誤會的 LLVM 跟 TVM
雖然 LLVM 跟 TVM 單字都有 Virtual Machine,但是和我們本篇的 Virtual Machine 沒什麼太大的關係,兩者都是編譯器框架,後者是針對深度學習的特化型編譯器。這兩者都著重在中間形式(IR)的最佳化,日後有時間會談,敬請期待。😁
LLVM 主要貢獻在統一中間碼格式,並且進行最佳化
TVM 主要貢獻在深度學習的圖優化,並且可以將深度學習模型編譯成任何平台(電腦、手機、平板、開發版等)都可以跑得程式碼,這個技術於我來說真的新穎。
# 2-5. 參考連結
What are the most popular virtual machines for programming languages? (opens new window)
Introducing NNVM Compiler: A New Open End-to-End Compiler for AI Frameworks (opens new window)
# 三、為什麼需要虛擬機 ? 有什麼缺點嗎 ?
# 3-1. 需要虛擬機的理由
除了你跟別人說我設計了一個虛擬機讓別人覺得你很酷以外,使用虛擬機其實有很多好處
自動垃圾回收功能
我們在寫 Python, Java, C#, Javascript 等程式語言,我們通常不太會去做記憶體的管理跟物件的生命週期(除非要做效能調校跟最佳化),通常虛擬機會幫我們標註跟回收記憶體,雖然 Golang 也編譯器幫你放垃圾回收的程式碼進去,C 跟 C++ 也有垃圾回收的函式庫可以用,但是就會變更麻煩。而且虛擬機的 bytecode 結構更容易分析,相對更容易實現自動垃圾回收機制。
Java 和 C# 的 Reflection 機制
Java 反射機制是指在 執行狀態 中,對於任意一個類,都能知道這個類的所有屬性和方法。對於任意一個物件,都能呼叫它的任意一個方法和屬性,這種動態獲取資訊及動態呼叫方法的功能成為Java的反射機制。透過虛擬機實現執行期的反射特性,可以讓程式碼更優雅。
安全性和穩健性
Java 利用 VM 來 隔一層再執行,拉開正在執行的程式跟作業系統和硬體的距離,所以你的程式碼不太容易影響到其他在作業系統的行程。相當於在一個獨立環境內做事情,因此安全性跟對於系統的穩健性有所幫助。
可攜性跟移植性、語言與特定的硬體架構分離
我們只需要將程式碼轉乘中間碼(byte code),之後藉由虛擬機直譯中間碼轉成不同平台對應的機器語言,所以我們只需要標準化中間碼,再分配給對應平台的虛擬機(例如 windows 上的 JVM,Unix 的 JVM 等) 就好,如此一來可以達到 Write once, run everywhere!
前面所提到的 LLVM 跟 TVM 就有虛擬機的味道再裡面,他們提供一個統一的中間格式,再幫你轉成對應的硬體架構程式碼。
強大的 JIT 機制
通常虛擬機直譯程式碼時,碰到迴圈往往會觸發及時編譯技術來幫我們最佳化程式碼。JIT 在動態編譯的時候可以得到比靜態編譯更多的訊息,雖然會付出記憶體的成本,但是可以在熱點程式碼(如 for-loop)進行最佳化。
創造自己的指令集
其實綜合一個字,就是懶 ,雖然開發虛擬機不是很容易,但我們可以利用虛擬機幫助我們做跨平台、執行程式,並且可以在程式語言的設計上更佳的靈活與彈性,對於記憶體的管理也相對不赤裸,大幅減輕程式設計人員的負擔與門檻。
# 3-2. 使用虛擬機的缺點
Performance
畢竟虛擬機幫我們做不少事情,垃圾回收、轉譯等事情,很多事情像垃圾回收的監控都會占用 CPU 執行時間,所以虛擬機的程式執行效率需要花更多力氣去做最佳化 (如 JIT 技術)。
Memory footprint
不論你的程式碼多小,都要載入一個虛擬機才能執行程式碼 (幾MB),也因為及時編譯的機制,十分消耗記憶體的資源。
直譯的 overhead
每一個指令都要 fetch、decoding、execution,有一定的成本,再來頻繁的間接跳躍指令,會造成 modern CPU 的 pipeline stall。
::: info 這種直譯的 for loop 搭配 switch case 有個專有名詞叫做 dispatch loop 或 dynamic dispatch :::
# 3-3. 參考連結
What is a VM and why do dynamic languages need one? (opens new window)
What are some of the advantages and disandvantages of the Java Virtual Machine? (opens new window)
is bytecode treated as instruction set for JVM? (opens new window)
# 四、 虛擬機和直譯器 ?
# 4-1. 虛擬機是一個技術的融合
現在的虛擬機,往往混合了編譯器、直譯器的成分,像 Java、C# 等都被稱做混合型語言。先編譯後直譯搭配及時編譯器使程式碼執行的效率接近編譯型語言。事實上,現在彼此之間的界線已經比較模糊了。
那為何 Python 常常被叫做直譯器執行,而 Java 則是虛擬機執行 ? 兩者不是都是先編譯後直譯搭配 JIT 嗎 ? 根據Java “Virtual Machine” vs. Python “Interpreter” parlance? (opens new window)
在這篇的解釋提到
- 虛擬機和直譯器的差別在於: 你是否認為他是和語言無關的 ?==
例如: JVM 就不是只有給 java 執行,kotlin, groovy, scala 等程式語言都可以跑在 JVM 上面,而 Python 虛擬機只有接受 python 語言。一旦有天有更多熱門的程式語言可以被編譯成 pyc (python byte code),那可能開始比較多人說跑在 Python 虛擬機上。
- 用戶體驗
通常我們在編譯 Java 時,需要顯式的編譯為 bytecode (.class檔案),然後執行。但是 Python 直接啟動就可以執行,就像是一般的腳本一樣。如果我們保留pyc文件,那機制會更像一個虛擬機。
- 歷史之壁
因為 Java 一開始就是為 byte code 解釋設計實現的,而 python 最初是從直譯器起家的。且對於 python 來說,虛擬機不是程式執行的必要手段,只是直譯器的擴展延伸。
- 語言型別
Java 是靜態檢查,Python 是動態檢查型別,動態檢查就是將編譯時間的成本轉嫁到執行時間,再來 Java 要宣告型別,而Python 不需要宣告型別,所以常常給人一種動態腳本語言的感覺,又會回到第二點的用戶體驗。
# 4-2. 參考連結
Whats-the-difference-between-an-interpreter-a-virtual-machine (opens new window)
Confused about advantage of interpreted language (opens new window)
Java “Virtual Machine” vs. Python “Interpreter” parlance? (opens new window)
# 五、結論
虛擬機的應用十分廣泛,目前的程式語言都是藉由虛擬機才得以執行,虛擬機的使用可以減輕一些開發者的負擔,讓語言的特性更加豐富簡練,到這裡你可能會問設計一個虛擬機器,我該做什麼、考慮什麼 ? 困難的地方在哪裡 ?
- 編譯器、直譯器、及時編譯器 : 不多說,每個都是大工程
- 中間碼最佳化 : 中間碼的最佳化是一個很龐大的工程,如何去妥善安排你的虛擬機指令達到最佳效率。
- 指令集的設計 : 設計一個虛擬機指令集。
- 垃圾回收(非必要但大多數虛擬機都有) : 用何種垃圾回收的演算法可以達到較好的效率、怎麼標註你所使用的資源。
- 跨平台 : 要在不同平台上執行你的虛擬機指令,有 n 種機器就要實現 n 種虛擬機。
當然你也可以從最簡單的開始實現,這也是我之後的目標之一。