跳轉至內容

鸚鵡虛擬機器/執行核心和操作碼

來自華夏公益教科書,開放世界開放書籍

執行核心

[編輯 | 編輯原始碼]

我們之前已經討論過執行核心,但在本章中,我們將深入討論它們。在這裡,我們將討論操作碼,以及將操作碼轉換為標準 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 () {
}

我們使用關鍵字 inout 定義輸入和輸出引數,後跟輸入型別。如果使用但沒有修改輸入引數,可以將其定義為 inconst 型別可以是 PMCSTR(字串)、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 命名和函式簽名

[編輯 | 編輯原始碼]

只要引數不同,您就可以擁有許多名稱相同的操作。以下兩個宣告是可以的

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::OpsFileParrot::Ops2c::*Parrot::OpsTrans::*

我們將在下面一節中檢視不同的執行核心。但總而言之,不同的執行核心需要以不同的格式編譯操作碼以供執行。因此,操作碼編譯器的任務相當複雜:它必須讀取操作碼描述檔案,並以幾種不同的輸出格式輸出語法正確的 C 程式碼。

Dynops: 動態操作碼庫

[編輯 | 編輯原始碼]

到目前為止,我們一直在討論的都是標準的內建操作。但是,這些並不是唯一可用的操作,鸚鵡還允許在執行時載入動態操作庫。

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 除錯核心
偵錯程式核心
分析核心
跟蹤核心


上一頁 鸚鵡虛擬機器 下一頁
IMCC 和 PIRC PMC 系統
華夏公益教科書