WebAssembly-添加新的操作碼(譯)

WebAssembly(Wasm)是基于堆棧的虛擬機的二進制指令格式。本教程將向讀者介紹在V8中實現(xiàn)新的WebAssembly指令的過程。

WebAssembly在V8中的實現(xiàn)分為三個部分:

  • 解釋器
  • 基線編譯器(Liftoff)
  • 優(yōu)化編譯器(TurboFan)

本文檔的其余部分重點介紹TurboFan管道,逐步介紹如何添加新的Wasm指令并在TurboFan中實現(xiàn)它。

在較高的層次上,Wasm指令被編譯為TurboFan圖,并且我們依靠TurboFan管道將圖編譯為(最終)機器代碼。有關(guān)TurboFan的更多信息,請查看V8文檔。

操作碼/指令

讓我們定義一個新指令,功能是給棧頂?shù)?a target="_blank">int32類型值加1。

注意:spec中可以找到被所有Wasm實現(xiàn)支持的指令列表。

所有Wasm指令都被定義在src/wasm/wasm-opcodes.h文件中。指令大致按其功能分組,例如控制,內(nèi)存,SIMD,原子等。

SIMD全稱Single Instruction Multiple Data,單指令多數(shù)據(jù)流,能夠復制多個操作數(shù),并把它們打包在大型寄存器的一組指令集。

讓我們添加新的指令I32Add1FOREACH_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操作碼并調(diào)用TurboFan圖形生成器。因此,根據(jù)操作碼的作用,您可能需要做更多的工作。為了簡潔起見,我們跳過此步驟。

為新的操作碼編寫測試

Wasm測試可以在test/cctest/wasm/中找到。讓我們看一下test/cctest/wasm/test-run-wasm.cc,其中測試了許多“簡單的”操作碼。

我們可以遵循此文件中的許多示例。常規(guī)設(shè)置為:

  • 創(chuàng)建一個 WasmRunner
  • 設(shè)置全局變量以保存結(jié)果(可選)
  • 將局部變量設(shè)置為指令的參數(shù)(可選)
  • 構(gòu)建wasm模塊
  • 運行它并與預(yù)期輸出進行比較

這是我們新操作碼的簡單測試:

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

提示:由于測試定義位于宏后面,因此查找測試名稱可能很棘手。使用代碼搜索單擊以發(fā)現(xiàn)宏定義。

此錯誤表明編譯器不知道我們的新指令。下一節(jié)將對此進行更改。

將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,并構(gòu)建一個TurboFan圖,該圖由帶有input輸入的操作Word32Equalinput是Wasm指令的參數(shù))和一個常數(shù)0組成。

Word32Equal操作符由底層的V8抽象機提供,該抽象機是體系結(jié)構(gòu)上獨立的。在后續(xù)的管道中,這個抽象的機器操作符將轉(zhuǎn)換為依賴于體系結(jié)構(gòu)的程序集。

對于我們的新操作碼,I32Add1,我們需要一個將常量1添加到輸入的圖,因此我們可以重新使用現(xiàn)有的機器操作符,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));

這足以使測試通過。但是,并非所有指令都有現(xiàn)成的TurboFan機器操作符。在這種情況下,我們必須將此新操作符添加到機器中。讓我們嘗試一下。

TurboFan機器操作符

我們想向TurboFan機器添加Int32Add1的內(nèi)容。因此,讓我們假裝它存在并首先使用它:

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. 機器可以理解的操作碼列表 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)

現(xiàn)在再次運行測試給我們帶來了另一個失?。?/p>

=== 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圖中的節(jié)點。但是,在匯編級別,我們有指令和操作數(shù)。指令選擇是將該圖轉(zhuǎn)換為指令和操作數(shù)的過程。

最后一個測試錯誤表明我們需要中的內(nèi)容src/compiler/backend/instruction-selector.cc。這是一個很大的文件,其中包含所有機器操作碼的巨大switch語句。它使用訪問者模式為每種類型的節(jié)點發(fā)出指令,從而調(diào)用特定于體系結(jié)構(gòu)的指令。

由于我們添加了新的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());

指令選擇依賴于體系結(jié)構(gòu),因此我們也必須將其添加到特定于體系結(jié)構(gòu)的指令選擇器文件中。對于此代碼示例,我們僅關(guān)注x64體系結(jié)構(gòu),因此需要對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)                               \

指令調(diào)度和代碼生成

運行我們的測試,我們看到新的編譯錯誤:

../../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.

指令調(diào)度要照顧到指令可能必須進行更多優(yōu)化(例如,指令重新排序)的依賴。我們的新操作碼沒有數(shù)據(jù)依賴性,因此我們可以將其簡單地添加到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:

代碼生成是我們將特定于體系結(jié)構(gòu)的操作碼轉(zhuǎn)換為匯編的地方。讓我們添加一個子句到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);

現(xiàn)在,我們將代碼生成留空,然后可以運行測試以確保所有代碼都能編譯:

=== 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).

因為我們的新指令尚未實現(xiàn),所以會發(fā)生這種失敗,因為它實際上是一個無操作指令,因此我們的實際值未更改(10)。

要實現(xiàn)我們的操作碼,我們可以使用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已經(jīng)實現(xiàn)。如果我們的新操作碼需要編寫新的匯編指令實現(xiàn),則可以將其添加到中src/compiler/backend/x64/assembler-x64.cc,其中匯編指令被編碼為字節(jié)并發(fā)出。

提示:要檢查生成的代碼,我們可以傳遞--print-codecctest。

其他架構(gòu)

在此代碼示例中,我們僅針對x64實現(xiàn)了此新指令。其他體系結(jié)構(gòu)所需的步驟相似:添加TurboFan機器操作碼,使用平臺相關(guān)的文件進行指令選擇,調(diào)度,代碼生成,匯編程序。

提示:如果編譯在另一個目標(例如arm64)上所做的工作,則很可能會在鏈接時出錯。要解決這些錯誤,請?zhí)砑?code>UNIMPLEMENTED()存根。

原文鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容