跳转至内容

鹦鹉虚拟机/运行核心和操作码

来自维基教科书,开放世界开放书籍

运行核心

[编辑 | 编辑源代码]

我们之前已经讨论过运行核心,但在本章中,我们将深入讨论它们。在这里,我们将讨论操作码,以及将操作码转换为标准 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 系统
华夏公益教科书