鸚鵡虛擬機器/執行核心和操作碼
我們之前已經討論過執行核心,但在本章中,我們將深入討論它們。在這裡,我們將討論操作碼,以及將操作碼轉換為標準 C 程式碼的特殊操作碼編譯器。我們還將看看這些操作碼是如何被操作碼編譯器翻譯成不同形式的,以及我們將看到執行這些操作碼的不同執行核心。
操作碼使用一種非常特殊的語法編寫,該語法混合了 C 和特殊關鍵字。操作碼由操作碼編譯器 tools/dev/ops2c.pl 轉換為不同執行核心所需的格式。
鸚鵡的核心操作碼都在 src/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 | 處理詞法和全域性變數的操作 |
操作使用 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 和一個整數,並返回一個數字。請注意引數沒有名稱。相反,它們對應於數字
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 傳遞給需要以 null 結尾的 C 字串的庫函式。以下操作無效
#include <string.h>
op my_str_length(out INT, in STR) {
$1 = strlen($2); // WRONG!
}
當我們在上面談論引數型別時,我們沒有完全完整。以下是在您的操作中可以使用的方向限定符列表
| 方向 | 意義 | 示例 |
|---|---|---|
| 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 指令載入到鸚鵡中。
執行核心是解碼和執行 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 除錯核心
- 偵錯程式核心
- 分析核心
- 跟蹤核心