WebAssembly(Wasm)是基于堆栈的虚拟机的二进制指令格式。本教程将向读者介绍在V8中实现新的WebAssembly指令的过程。
WebAssembly在V8中的实现分为三个部分:
- 解释器
- 基线编译器(Liftoff)
- 优化编译器(TurboFan)
本文档的其余部分重点介绍TurboFan管道,逐步介绍如何添加新的Wasm指令并在TurboFan中实现它。
在较高的层次上,Wasm指令被编译为TurboFan图,并且我们依靠TurboFan管道将图编译为(最终)机器代码。有关TurboFan的更多信息,请查看V8文档。
操作码/指令
让我们定义一个新指令,功能是给栈顶的int32
类型值加1
。
注意:spec中可以找到被所有Wasm实现支持的指令列表。
所有Wasm指令都被定义在src/wasm/wasm-opcodes.h
文件中。指令大致按其功能分组,例如控制,内存,SIMD,原子等。
SIMD全称Single Instruction Multiple Data,单指令多数据流,能够复制多个操作数,并把它们打包在大型寄存器的一组指令集。
让我们添加新的指令I32Add1
到FOREACH_SIMPLE_OPCODE
部分:
diff --git a/src/wasm/wasm-opcodes.h b/src/wasm/wasm-opcodes.h
index 6970c667e7..867cbf451a 100644
--- a/src/wasm/wasm-opcodes.h
+++ b/src/wasm/wasm-opcodes.h
@@ -96,6 +96,7 @@ bool IsJSCompatibleSignature(const FunctionSig* sig, bool hasBigIntFeature);
// Expressions with signatures.
#define FOREACH_SIMPLE_OPCODE(V) \
+ V(I32Add1, 0xee, i_i) \
V(I32Eqz, 0x45, i_i) \
V(I32Eq, 0x46, i_ii) \
V(I32Ne, 0x47, i_ii) \
WebAssembly是二进制格式,因此0xee
指定了该指令的编码。在本教程中,我们选择了0xee
因为该编码当前未被使用。
注意:实际上,在spec中添加指令所涉及的工作超出了此处描述的范围。
我们可以使用以下命令对操作码运行简单的单元测试:
$ tools/dev/gm.py x64.debug unittests/WasmOpcodesTest*
...
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from WasmOpcodesTest
[ RUN ] WasmOpcodesTest.EveryOpcodeHasAName
../../test/unittests/wasm/wasm-opcodes-unittest.cc:27: Failure
Value of: false
Actual: false
Expected: true
WasmOpcodes::OpcodeName(kExprI32Add1) == "unknown"; plazz halp in src/wasm/wasm-opcodes.cc
[ FAILED ] WasmOpcodesTest.EveryOpcodeHasAName
该错误表明我们没有为新指令赋予名称。为新的操作码添加名称可以在src/wasm/wasm-opcodes.cc
文件中完成:
diff --git a/src/wasm/wasm-opcodes.cc b/src/wasm/wasm-opcodes.cc
index 5ed664441d..2d4e9554fe 100644
--- a/src/wasm/wasm-opcodes.cc
+++ b/src/wasm/wasm-opcodes.cc
@@ -75,6 +75,7 @@ const char* WasmOpcodes::OpcodeName(WasmOpcode opcode) {
// clang-format off
// Standard opcodes
+ CASE_I32_OP(Add1, "add1")
CASE_INT_OP(Eqz, "eqz")
CASE_ALL_OP(Eq, "eq")
CASE_I64x2_OP(Eq, "eq")
通过在FOREACH_SIMPLE_OPCODE
中添加新指令,我们将跳过在src/wasm/function-body-decoder-impl.h
中完成的大量工作,该工作将解码Wasm操作码并调用TurboFan图形生成器。因此,根据操作码的作用,您可能需要做更多的工作。为了简洁起见,我们跳过此步骤。
为新的操作码编写测试
Wasm测试可以在test/cctest/wasm/
中找到。让我们看一下test/cctest/wasm/test-run-wasm.cc
,其中测试了许多“简单的”操作码。
我们可以遵循此文件中的许多示例。常规设置为:
- 创建一个
WasmRunner
- 设置全局变量以保存结果(可选)
- 将局部变量设置为指令的参数(可选)
- 构建wasm模块
- 运行它并与预期输出进行比较
这是我们新操作码的简单测试:
diff --git a/test/cctest/wasm/test-run-wasm.cc b/test/cctest/wasm/test-run-wasm.cc
index 26df61ceb8..b1ee6edd71 100644
--- a/test/cctest/wasm/test-run-wasm.cc
+++ b/test/cctest/wasm/test-run-wasm.cc
@@ -28,6 +28,15 @@ namespace test_run_wasm {
#define RET(x) x, kExprReturn
#define RET_I8(x) WASM_I32V_2(x), kExprReturn
+#define WASM_I32_ADD1(x) x, kExprI32Add1
+
+WASM_EXEC_TEST(Int32Add1) {
+ WasmRunner<int32_t> r(execution_tier);
+ // 10 + 1
+ BUILD(r, WASM_I32_ADD1(WASM_I32V_1(10)));
+ CHECK_EQ(11, r.Call());
+}
+
WASM_EXEC_TEST(Int32Const) {
WasmRunner<int32_t> r(execution_tier);
const int32_t kExpectedValue = 0x11223344;
运行测试:
$ tools/dev/gm.py x64.debug 'cctest/test-run-wasm-simd/RunWasmTurbofan_I32Add1'
...
=== cctest/test-run-wasm/RunWasmTurbofan_Int32Add1 ===
#
# Fatal error in ../../src/compiler/wasm-compiler.cc, line 988
# Unsupported opcode 0xee:i32.add1
提示:由于测试定义位于宏后面,因此查找测试名称可能很棘手。使用代码搜索单击以发现宏定义。
此错误表明编译器不知道我们的新指令。下一节将对此进行更改。
将Wasm编译到TurboFan
在引言中,我们提到了Wasm指令被编译为TurboFan图。wasm-compiler.cc
是进行该编译工作的文件。让我们来看一个示例操作码,I32Eqz
:
switch (opcode) {
case wasm::kExprI32Eqz:
op = m->Word32Equal();
return graph()->NewNode(op, input, mcgraph()->Int32Constant(0));
这将打开Wasm操作码wasm::kExprI32Eqz
,并构建一个TurboFan图,该图由带有input
输入的操作Word32Equal
(input
是Wasm指令的参数)和一个常数0
组成。
Word32Equal
操作符由底层的V8抽象机提供,该抽象机是体系结构上独立的。在后续的管道中,这个抽象的机器操作符将转换为依赖于体系结构的程序集。
对于我们的新操作码,I32Add1
,我们需要一个将常量1添加到输入的图,因此我们可以重新使用现有的机器操作符,Int32Add
,将输入传递给它,并使用常量1:
diff --git a/src/compiler/wasm-compiler.cc b/src/compiler/wasm-compiler.cc
index f666bbb7c1..399293c03b 100644
--- a/src/compiler/wasm-compiler.cc
+++ b/src/compiler/wasm-compiler.cc
@@ -713,6 +713,8 @@ Node* WasmGraphBuilder::Unop(wasm::WasmOpcode opcode, Node* input,
const Operator* op;
MachineOperatorBuilder* m = mcgraph()->machine();
switch (opcode) {
+ case wasm::kExprI32Add1:
+ return graph()->NewNode(m->Int32Add(), input, mcgraph()->Int32Constant(1));
case wasm::kExprI32Eqz:
op = m->Word32Equal();
return graph()->NewNode(op, input, mcgraph()->Int32Constant(0));
这足以使测试通过。但是,并非所有指令都有现成的TurboFan机器操作符。在这种情况下,我们必须将此新操作符添加到机器中。让我们尝试一下。
TurboFan机器操作符
我们想向TurboFan机器添加Int32Add1
的内容。因此,让我们假装它存在并首先使用它:
diff --git a/src/compiler/wasm-compiler.cc b/src/compiler/wasm-compiler.cc
index f666bbb7c1..1d93601584 100644
--- a/src/compiler/wasm-compiler.cc
+++ b/src/compiler/wasm-compiler.cc
@@ -713,6 +713,8 @@ Node* WasmGraphBuilder::Unop(wasm::WasmOpcode opcode, Node* input,
const Operator* op;
MachineOperatorBuilder* m = mcgraph()->machine();
switch (opcode) {
+ case wasm::kExprI32Add1:
+ return graph()->NewNode(m->Int32Add1(), input);
case wasm::kExprI32Eqz:
op = m->Word32Equal();
return graph()->NewNode(op, input, mcgraph()->Int32Constant(0));
尝试运行相同的测试会导致编译失败,提示在哪里进行更改:
../../src/compiler/wasm-compiler.cc:717:34: error: no member named 'Int32Add1' in 'v8::internal::compiler::MachineOperatorBuilder'; did you mean 'Int32Add'?
return graph()->NewNode(m->Int32Add1(), input);
^~~~~~~~~
Int32Add
有几个地方需要修改以添加运算符:
src/compiler/machine-operator.cc
- 头文件
src/compiler/machine-operator.h
- 机器可以理解的操作码列表
src/compiler/opcodes.h
- 验证器
src/compiler/verifier.cc
diff --git a/src/compiler/machine-operator.cc b/src/compiler/machine-operator.cc
index 16e838c2aa..fdd6d951f0 100644
--- a/src/compiler/machine-operator.cc
+++ b/src/compiler/machine-operator.cc
@@ -136,6 +136,7 @@ MachineType AtomicOpType(Operator const* op) {
#define MACHINE_PURE_OP_LIST(V) \
PURE_BINARY_OP_LIST_32(V) \
PURE_BINARY_OP_LIST_64(V) \
+ V(Int32Add1, Operator::kNoProperties, 1, 0, 1) \
V(Word32Clz, Operator::kNoProperties, 1, 0, 1) \
V(Word64Clz, Operator::kNoProperties, 1, 0, 1) \
V(Word32ReverseBytes, Operator::kNoProperties, 1, 0, 1) \
diff --git a/src/compiler/machine-operator.h b/src/compiler/machine-operator.h
index a2b9fce0ee..f95e75a445 100644
--- a/src/compiler/machine-operator.h
+++ b/src/compiler/machine-operator.h
@@ -265,6 +265,8 @@ class V8_EXPORT_PRIVATE MachineOperatorBuilder final
const Operator* Word32PairShr();
const Operator* Word32PairSar();
+ const Operator* Int32Add1();
+
const Operator* Int32Add();
const Operator* Int32AddWithOverflow();
const Operator* Int32Sub();
diff --git a/src/compiler/opcodes.h b/src/compiler/opcodes.h
index ce24a0bd3f..2c8c5ebaca 100644
--- a/src/compiler/opcodes.h
+++ b/src/compiler/opcodes.h
@@ -506,6 +506,7 @@
V(Float64LessThanOrEqual)
#define MACHINE_UNOP_32_LIST(V) \
+ V(Int32Add1) \
V(Word32Clz) \
V(Word32Ctz) \
V(Int32AbsWithOverflow) \
diff --git a/src/compiler/verifier.cc b/src/compiler/verifier.cc
index 461aef0023..95251934ce 100644
--- a/src/compiler/verifier.cc
+++ b/src/compiler/verifier.cc
@@ -1861,6 +1861,7 @@ void Verifier::Visitor::Check(Node* node, const AllNodes& all) {
case IrOpcode::kSignExtendWord16ToInt64:
case IrOpcode::kSignExtendWord32ToInt64:
case IrOpcode::kStaticAssert:
+ case IrOpcode::kInt32Add1:
#define SIMD_MACHINE_OP_CASE(Name) case IrOpcode::k##Name:
MACHINE_SIMD_OP_LIST(SIMD_MACHINE_OP_CASE)
现在再次运行测试给我们带来了另一个失败:
=== cctest/test-run-wasm/RunWasmTurbofan_Int32Add1 ===
#
# Fatal error in ../../src/compiler/backend/instruction-selector.cc, line 2072
# Unexpected operator #289:Int32Add1 @ node #7
指令选择
到目前为止,我们一直在TurboFan级别上工作,处理TurboFan图中的节点。但是,在汇编级别,我们有指令和操作数。指令选择是将该图转换为指令和操作数的过程。
最后一个测试错误表明我们需要中的内容src/compiler/backend/instruction-selector.cc
。这是一个很大的文件,其中包含所有机器操作码的巨大switch语句。它使用访问者模式为每种类型的节点发出指令,从而调用特定于体系结构的指令。
由于我们添加了新的TurboFan机器操作码,因此我们也需要在此处添加它:
diff --git a/src/compiler/backend/instruction-selector.cc b/src/compiler/backend/instruction-selector.cc
index 3152b2d41e..7375085649 100644
--- a/src/compiler/backend/instruction-selector.cc
+++ b/src/compiler/backend/instruction-selector.cc
@@ -2067,6 +2067,8 @@ void InstructionSelector::VisitNode(Node* node) {
return MarkAsWord32(node), VisitS1x16AnyTrue(node);
case IrOpcode::kS1x16AllTrue:
return MarkAsWord32(node), VisitS1x16AllTrue(node);
+ case IrOpcode::kInt32Add1:
+ return MarkAsWord32(node), VisitInt32Add1(node);
default:
FATAL("Unexpected operator #%d:%s @ node #%d", node->opcode(),
node->op()->mnemonic(), node->id());
指令选择依赖于体系结构,因此我们也必须将其添加到特定于体系结构的指令选择器文件中。对于此代码示例,我们仅关注x64体系结构,因此需要对src/compiler/backend/x64/instruction-selector-x64.cc
进行修改:
diff --git a/src/compiler/backend/x64/instruction-selector-x64.cc b/src/compiler/backend/x64/instruction-selector-x64.cc
index 2324e119a6..4b55671243 100644
--- a/src/compiler/backend/x64/instruction-selector-x64.cc
+++ b/src/compiler/backend/x64/instruction-selector-x64.cc
@@ -841,6 +841,11 @@ void InstructionSelector::VisitWord32ReverseBytes(Node* node) {
Emit(kX64Bswap32, g.DefineSameAsFirst(node), g.UseRegister(node->InputAt(0)));
}
+void InstructionSelector::VisitInt32Add1(Node* node) {
+ X64OperandGenerator g(this);
+ Emit(kX64Int32Add1, g.DefineSameAsFirst(node), g.UseRegister(node->InputAt(0)));
+}
+
我们还需要将此新的特定kX64Int32Add1
于x64的操作码添加到src/compiler/backend/x64/instruction-codes-x64.h
:
diff --git a/src/compiler/backend/x64/instruction-codes-x64.h b/src/compiler/backend/x64/instruction-codes-x64.h
index 9b8be0e0b5..7f5faeb87b 100644
--- a/src/compiler/backend/x64/instruction-codes-x64.h
+++ b/src/compiler/backend/x64/instruction-codes-x64.h
@@ -12,6 +12,7 @@ namespace compiler {
// X64-specific opcodes that specify which assembly sequence to emit.
// Most opcodes specify a single instruction.
#define TARGET_ARCH_OPCODE_LIST(V) \
+ V(X64Int32Add1) \
V(X64Add) \
V(X64Add32) \
V(X64And) \
指令调度和代码生成
运行我们的测试,我们看到新的编译错误:
../../src/compiler/backend/x64/instruction-scheduler-x64.cc:15:11: error: enumeration value 'kX64Int32Add1' not handled in switch [-Werror,-Wswitch]
switch (instr->arch_opcode()) {
^
1 error generated.
...
../../src/compiler/backend/x64/code-generator-x64.cc:733:11: error: enumeration value 'kX64Int32Add1' not handled in switch [-Werror,-Wswitch]
switch (arch_opcode) {
^
1 error generated.
指令调度要照顾到指令可能必须进行更多优化(例如,指令重新排序)的依赖。我们的新操作码没有数据依赖性,因此我们可以将其简单地添加到src/compiler/backend/x64/instruction-scheduler-x64.cc
:
diff --git a/src/compiler/backend/x64/instruction-scheduler-x64.cc b/src/compiler/backend/x64/instruction-scheduler-x64.cc
index 79eda7e78d..3667a84577 100644
--- a/src/compiler/backend/x64/instruction-scheduler-x64.cc
+++ b/src/compiler/backend/x64/instruction-scheduler-x64.cc
@@ -13,6 +13,7 @@ bool InstructionScheduler::SchedulerSupported() { return true; }
int InstructionScheduler::GetTargetInstructionFlags(
const Instruction* instr) const {
switch (instr->arch_opcode()) {
+ case kX64Int32Add1:
case kX64Add:
case kX64Add32:
case kX64And:
代码生成是我们将特定于体系结构的操作码转换为汇编的地方。让我们添加一个子句到src/compiler/backend/x64/code-generator-x64.cc
:
diff --git a/src/compiler/backend/x64/code-generator-x64.cc b/src/compiler/backend/x64/code-generator-x64.cc
index 61c3a45a16..9c37ed7464 100644
--- a/src/compiler/backend/x64/code-generator-x64.cc
+++ b/src/compiler/backend/x64/code-generator-x64.cc
@@ -731,6 +731,9 @@ CodeGenerator::CodeGenResult CodeGenerator::AssembleArchInstruction(
InstructionCode opcode = instr->opcode();
ArchOpcode arch_opcode = ArchOpcodeField::decode(opcode);
switch (arch_opcode) {
+ case kX64Int32Add1: {
+ break;
+ }
case kArchCallCodeObject: {
if (HasImmediateInput(instr, 0)) {
Handle<Code> code = i.InputCode(0);
现在,我们将代码生成留空,然后可以运行测试以确保所有代码都能编译:
=== cctest/test-run-wasm/RunWasmTurbofan_Int32Add1 ===
#
# Fatal error in ../../test/cctest/wasm/test-run-wasm.cc, line 37
# Check failed: 11 == r.Call() (11 vs. 10).
因为我们的新指令尚未实现,所以会发生这种失败,因为它实际上是一个无操作指令,因此我们的实际值未更改(10
)。
要实现我们的操作码,我们可以使用add
汇编指令:
diff --git a/src/compiler/backend/x64/code-generator-x64.cc b/src/compiler/backend/x64/code-generator-x64.cc
index 6c828d6bc4..260c8619f2 100644
--- a/src/compiler/backend/x64/code-generator-x64.cc
+++ b/src/compiler/backend/x64/code-generator-x64.cc
@@ -744,6 +744,11 @@ CodeGenerator::CodeGenResult CodeGenerator::AssembleArchInstruction(
InstructionCode opcode = instr->opcode();
ArchOpcode arch_opcode = ArchOpcodeField::decode(opcode);
switch (arch_opcode) {
+ case kX64Int32Add1: {
+ DCHECK_EQ(i.OutputRegister(), i.InputRegister(0));
+ __ addl(i.InputRegister(0), Immediate(1));
+ break;
+ }
case kArchCallCodeObject: {
if (HasImmediateInput(instr, 0)) {
Handle<Code> code = i.InputCode(0);
这使测试通过:
幸运的是,对我们来说addl
已经实现。如果我们的新操作码需要编写新的汇编指令实现,则可以将其添加到中src/compiler/backend/x64/assembler-x64.cc
,其中汇编指令被编码为字节并发出。
提示:要检查生成的代码,我们可以传递
--print-code
给cctest
。
其他架构
在此代码示例中,我们仅针对x64实现了此新指令。其他体系结构所需的步骤相似:添加TurboFan机器操作码,使用平台相关的文件进行指令选择,调度,代码生成,汇编程序。
提示:如果编译在另一个目标(例如arm64)上所做的工作,则很可能会在链接时出错。要解决这些错误,请添加UNIMPLEMENTED()
存根。