鸚鵡虛擬機器/執行核心和操作碼
我們之前討論過執行核心,但在本章中,我們將更深入地討論它們。在這裡,我們將討論操作碼以及將操作碼轉換為標準 C 程式碼的特殊操作碼編譯器。我們還將瞭解操作碼編譯器如何將這些操作碼轉換為不同的形式,以及執行這些操作碼的不同執行核心。
操作碼使用一種非常特殊的語法編寫,它混合了 C 和特殊關鍵字。操作碼由操作碼編譯器 tools/dev/ops2c.pl 轉換為不同執行核心所需的格式。
鸚鵡的核心操作碼都在 src/ops/ 中定義,位於副檔名為 *.ops 的檔案中。操作碼根據其目的分為不同的檔案
| Ops 檔案 | 目的 |
|---|---|
| bit.ops | 按位邏輯運算 |
| cmp.ops | 比較操作 |
| core.ops | 基本鸚鵡操作、私有內部操作、控制流、併發、事件和異常。 |
| debug.ops | 用於除錯鸚鵡和 HLL 程式的操作。 |
| experimental.ops | 正在測試的操作,可能不穩定。不要依賴這些操作。 |
| io.ops | 用於處理檔案和終端的輸入和輸出的操作。 |
| math.ops | 數學運算 |
| object.ops | 用於處理面向物件細節的操作 |
| obscure.ops | 用於模糊和專門的三角函式的操作 |
| pic.ops | 多型內聯快取的私有操作碼。不要使用這些。 |
| pmc.ops | 用於處理 PMC、建立 PMC 的操作碼。用於處理類似陣列的 PMC(push、pop、shift、unshift)和類似雜湊的 PMC 的通用操作 |
| set.ops | 用於設定和載入暫存器的操作 |
| stm.ops | 用於軟體事務記憶體的操作,這是鸚鵡的執行緒間通訊系統。實際上,這些操作不會被使用,而是使用 STMRef 和 STMVar PMC。 |
| string.ops | 用於處理字串的操作 |
| sys.ops | 用於與底層系統互動的操作 |
| var.ops | 用於處理詞法變數和全域性變數的操作 |
Ops 使用 op 關鍵字定義,其工作方式類似於 C 原始碼。以下是一個例子
op my_op () {
}
或者,我們也可以使用 inline 關鍵字
inline op my_op () {
}
我們使用 in 和 out 關鍵字定義輸入和輸出引數,然後是輸入型別。如果使用了輸入引數但沒有改變,可以將其定義為 inconst。型別可以是 PMC、STR(字串)、NUM(浮點數)或 INT(整數)。以下是一個函式原型示例
op my_op(out NUM, in STR, in PMC, in INT) {
}
該函式接受一個字串、一個 PMC 和一個 int,並返回一個 num。請注意引數沒有名稱。相反,它們對應於數字
op my_op(out NUM, in STR, in PMC, in INT)
^ ^ ^ ^
| | | |
$1 $2 $3 $4
以下是一個示例,一個操作接收三個整數輸入,將它們加在一起,並返回一個整數總和
op sum(out INT, in INT, in INT, in INT) {
$1 = $2 + $3 + $4;
}
Nums 被轉換為普通的浮點數,因此可以直接傳遞給需要浮點數或雙精度浮點數的函式。同樣,INTs 只是基本整數,可以像這樣對待。但是,PMC 和 STRING 是複雜的值。不能將鸚鵡 STRING 傳遞給需要以空字元結尾的 C 字串的庫函式。以下程式碼是錯誤的
#include <string.h>
op my_str_length(out INT, in STR) {
$1 = strlen($2); // WRONG!
}
當我們談論上面的引數型別時,我們並沒有完全完整。以下是您可以在 op 中包含的指示限定符列表
| 方向 | 含義 | 例子 |
|---|---|---|
| in | 引數是輸入 | op my_op(in INT) |
| out | 引數是輸出 | op pi(out NUM) {
$1 = 3.14;
}
|
| inout | 引數是輸入和輸出 | op increment(inout INT) {
$1 = $1 + 1;
}
|-
| inconst || The input parameter is constant, it is not modified
| <pre>
op double_const(out INT, inconst INT) {
$1 = $2 + $2;
}
以及在 PIR 中 $I0 = double_const 5 # numeric literal "5" is a constant |
| invar | 輸入引數是一個變數,例如 PMC | op my_op(invar PMC) |
引數的型別也可以是以下幾種選項之一
| 型別 | 含義 | 例子 |
|---|---|---|
| INT | 整數值 | 42 或 $I0 |
| NUM | 浮點數 | 3.14 或 $N3 |
| STR | 字串 | "Hello" 或 $S4 |
| PMC | PMC 變數 | $P0 |
| KEY | 雜湊鍵 | ["name"] |
| INTKEY | 整數索引 | [5] |
| LABEL | 程式碼中要跳轉到的位置 | jump_here |
只要引數不同,就可以有多個具有相同名稱的操作。以下兩個宣告是正確的
op my_op (out INT, in INT) {
}
op my_op (out NUM, in INT) {
}
操作編譯器將這些操作宣告轉換為類似於以下 C 函式宣告的內容
INTVAL op_my_op_i_i(INTVAL param1) {
}
NUMBER op_my_op_n_i(INTVAL param1) {
}
請注意函式名稱末尾的 "_i_i" 和 "_n_i" 字尾?這是鸚鵡確保函式名稱在系統中唯一的機制,以防止編譯器問題。這也可以輕鬆地檢視函式簽名並瞭解它接受哪些型別的運算元。
操作碼可以確定執行完成後控制流將轉移到哪裡。對於大多數操作碼,預設行為是轉移到記憶體中的下一個指令。但是,有多種方法可以改變控制流,其中一些方法非常新穎且奇特。有幾個關鍵字可以用來獲取操作的地址。然後我們可以直接 goto 該指令,或者我們可以儲存該地址並稍後跳轉到它。
| 關鍵字 | 含義 |
|---|---|
| NEXT() | 跳轉到記憶體中的下一個操作碼 |
| ADDRESS(a) | 跳轉到 a 給出的操作碼。a 的型別為 opcode_t*。 |
| OFFSET(a) | 跳轉到當前偏移量 a 偏移量給出的操作碼。a 通常是型別 in LABEL。 |
| POP() | 獲取控制堆疊頂部的地址。此功能正在棄用,最終鸚鵡將在內部無堆疊。 |
操作碼編譯器位於 dev/build/ops2c.pl 中,但其大多數功能位於各種包含的庫中,例如 Parrot::OpsFile。Parrot::Ops2c::* 和 Parrot::OpsTrans::*。
我們將在下一節中介紹不同的執行核心。需要說明的是,不同的執行核心要求操作碼被編譯成不同的格式才能執行。因此,操作碼編譯器的任務比較複雜:它必須讀取操作碼描述檔案並以多種不同的輸出格式輸出語法正確的 C 程式碼。
到目前為止,我們一直在討論的都是標準的內建操作。但是,這些並不是唯一可用的操作,鸚鵡還允許在執行時載入動態操作庫。
dynops 是動態載入的操作庫。它們幾乎與常規的內建操作寫法相同,但它們被單獨編譯成庫,並在執行時使用 `loadlib` 指令載入到 Parrot 中。
執行核心是解碼和執行 PBC 檔案中操作碼流的程式。在最簡單的情況下,執行核心是一個迴圈,它接收每個位元組碼值,從 PBC 流中收集引數資料,並將控制權傳遞給操作碼例程以執行。
有多種不同的操作核心。有些非常實用和簡單,有些使用特殊技巧和編譯器功能來最佳化速度。有些操作核心執行有用的輔助任務,如除錯和分析。有些執行核心沒有實際用途,只是為了滿足一些基本的學術興趣。
- 慢速核心
- 在慢速核心中,每個操作碼都被編譯成一個單獨的函式。每個操作碼函式接受兩個引數:指向當前操作碼的指標,以及 Parrot 直譯器結構。操作碼的所有引數都被解析並存儲在直譯器結構中,以便檢索。顧名思義,這個核心非常慢。但是,它的概念非常簡單,而且非常穩定。由於這個原因,慢速核心被用作我們將在後面討論的一些專用核心的基礎。
- 快速核心
- 快速核心與慢速核心完全相同,只是它沒有執行慢速核心執行的邊界檢查和顯式上下文更新。
- 切換核心
- 切換核心使用一個巨大的 C `switch { }` 語句來處理操作碼排程,而不是使用單獨的函式。好處是,不需要為每個操作碼呼叫函式,從而節省了呼叫操作碼所需的機器程式碼指令數量。
- JIT 核心
- Exec 核心
接下來我們將討論的兩個核心依賴於某些編譯器的專用功能,稱為**計算 goto**。在正常的 ANSI C 中,標籤是控制流語句,不被視為一等公民。但是,支援計算 goto 的編譯器允許將標籤視為指標,儲存在變數中,並間接跳轉到它們。
void * my_label = &&THE_LABEL; goto *my_label;
計算 goto 核心將所有操作碼編譯成一個大的函式,每個操作碼對應於函式中的一個標籤。所有這些標籤都儲存在一個大型陣列中
void *opcode_labels[] = {
&&opcode1,
&&opcode2,
&&opcode3,
...
};
然後,每個操作碼值都可以作為此陣列的偏移量,如下所示
goto *opcode_labels[current_opcode];
- 計算 goto 核心
- 計算 goto 核心使用上面描述的機制來排程不同的操作碼。在執行完每個操作碼後,會查詢傳入位元組碼流中的下一個操作碼,並從那裡進行排程。
- 預引用計算 goto 核心
- 在預計算 goto 核心中,位元組碼流被預處理以將操作碼編號轉換為相應的標籤。這意味著它們不需要每次都查詢,操作碼可以直接跳轉到,就像它是一個標籤一樣。請記住,排程機制必須在每個操作碼之後使用,在大型程式中可能存在數百萬個操作碼。即使在操作碼之間的機器程式碼指令數量上略有節省,也會對速度產生巨大影響。
- GC 除錯核心
- 偵錯程式核心
- 分析核心
- 跟蹤核心