WebAssembly - 新增一個操作碼

WebAssembly (Wasm) 是基於堆疊的虛擬機器二進制指令格式。本教學課程將引導讀者在 V8 中實作新的 WebAssembly 指令。

WebAssembly 在 V8 中分為三個部分實作

本文檔的其餘部分將重點放在 TurboFan 管線上,說明如何新增新的 Wasm 指令並在 TurboFan 中實作它。

在高階層級中,Wasm 指令會編譯成 TurboFan 圖形,我們仰賴 TurboFan 管線將圖形編譯成 (最終) 機器碼。有關 TurboFan 的更多資訊,請查看 V8 文件

操作碼/指令 #

讓我們定義一個新的指令,將 1 加到堆疊頂端的 int32

注意:可以在 規格中找到所有 Wasm 實作支援的指令清單。

所有 Wasm 指令都在 src/wasm/wasm-opcodes.h 中定義。這些指令大致依據它們的功能進行分組,例如控制、記憶體、SIMD、原子等。

讓我們將新的指令 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,因為它目前未使用。

注意:實際將指令新增到規格中涉及超出本文所述的作業。

我們可以使用下列指令執行操作碼的簡單單元測試

$ 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 圖形產生器。因此,根據您的操作碼功能,您可能需要執行更多作業。我們在此略過這些作業以求簡潔。

為新的操作碼撰寫測試 #

可以在 test/cctest/wasm/ 中找到 Wasm 測試。讓我們來看看 test/cctest/wasm/test-run-wasm.cc,其中測試許多「簡單」操作碼。

這個檔案中有許多範例可供我們遵循。一般設定如下

以下是我們新的操作碼的簡單測試

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 是發生此情況的地方。讓我們來看一個範例 opcode,I32Eqz

  switch (opcode) {
case wasm::kExprI32Eqz:
op = m->Word32Equal();
return graph()->NewNode(op, input, mcgraph()->Int32Constant(0));

這會切換 Wasm opcode wasm::kExprI32Eqz,並建立一個 TurboFan 圖形,其中包含運算 Word32Equal,其輸入為 input,這是 Wasm 指令的引數,以及常數 0

Word32Equal 算子是由底層 V8 抽象機器提供的,它與架構無關。在管線的後續部分,這個抽象機器算子會轉譯成與架構相關的組譯。

對於我們的 opcode 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 機器算子 #

我們想要將 Int32Add1 的知識加入 TurboFan 機器。因此,讓我們假裝它存在,並先使用它

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

有幾個地方需要修改才能加入算子

  1. src/compiler/machine-operator.cc
  2. 標頭 src/compiler/machine-operator.h
  3. 機器了解的 opcode 清單 src/compiler/opcodes.h
  4. 驗證器 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)));
+}
+

我們也需要將這個新的特定於 x64 的操作碼 kX64Int32Add1 新增到 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() 存根程式。