《Chrome V8源码》27.神秘又简单的dispatch_table_

 

1 摘要

本篇文章是Builtin专题的第三篇,讲解Bytecode的执行、数据结构以及Dispatch。dispatchtable是连接Bytecode之间的纽带,它记录了每条Bytecode handler的地址,Ignition通过dispatchtable查找并执行相应的Bytecode。本文内容组织方法:Bytecode的执行和数据结构(章节2);Bytecode的调度(章节3)。

 

2 Bytecode的执行

在V8中,负责执行Bytecode的解释器是Ignition,Ignition执行Bytecode时要做很多复杂的准备工作,这些“准备工作”后续文章讲解,我们重点说明Bytecode的执行。
Bytecode以JavaScript函数为粒度生成并存储在Bytecode array中,即Bytecode array是存储Bytecode的数组,源码如下:

1.  class BytecodeArray : public FixedArrayBase {
2.   public:
3.    static constexpr int SizeFor(int length) {
4.      return OBJECT_POINTER_ALIGN(kHeaderSize + length);
5.    }
6.    inline byte get(int index) const;
7.    inline void set(int index, byte value);
8.    inline Address GetFirstBytecodeAddress();
9.  //省略...................
10.  };

我们仅说明与本文有关的两点内容:
(1) 第3行代码SizeFor(int length)计算Bytecode array的长度,参数length的值是编译JavaScript函数后得到的Bytecode的数量,length+Bytecode array需要的空间等于Bytecode array的长度。在创建Bytecode array时,使用SizeFor(int length)计算申请内存的长度;
(2) 第8行代码GetFirstBytecodeAddress()获取Bytecode的首地址。把Parser生成的Bytecode拷贝到Bytecode array时会用到该函数。
Factory::NewBytecodeArray()中,使用SizeFor(int length)的返回值申请内存,用CopyBytes()把Bytecode拷贝到首地址中。下面是一段Bytecode源码:

1.  a7                StackCheck
2.  12 00             LdaConstant [0]
3.  15 01 00          StaGlobal [1], [0]
4.  13 01 02          LdaGlobal [1], [2]
5.  26 f9             Star r2
6.  29 f9 02          LdaNamedPropertyNoFeedback r2, [2]
7.  26 fa             Star r1
8.  0c 05             LdaSmi [5]
9.  Constant pool (size = 6)
10.  0000005EAE403019: [FixedArray] in OldSpace
11.   - map: 0x03d0be000169 <Map>
12.   - length: 6
13.             0: 0x005eae402f59 <String[#22]: ignoreCase here we go!>
14.             1: 0x038ee90c3cc1 <String[#1]: a>
15.             2: 0x01b92bc2bde1 <String[#9]: substring>
16.             3: 0x038ee90c3fa9 <String[#1]: b>
17.             4: 0x01b92bc33839 <String[#7]: console>
18.             5: 0x01b92bc32e79 <String[#3]: log>

第2行代码12 00 LdaConstant [0]12是LdaConstant的编号,这个编号也是LdaConstant的枚举值,即Bytecode[0x12]=kLdaConstant,源码如下:

enum class Bytecode : uint8_t {

        kWide, kExtraWide, kDebugBreakWide, kDebugBreakExtraWide, kDebugBreak0, kDebugBreak1, kDebugBreak2, kDebugBreak3, kDebugBreak4, kDebugBreak5, kDebugBreak6, kLdaZero, kLdaSmi, kLdaUndefined, kLdaNull, kLdaTheHole, kLdaTrue, kLdaFalse, kLdaConstant, kLdaGlobal, kLdaGlobalInsideTypeof, kStaGlobal, kPushContext, kPopContext, kLdaContextSlot, kLdaImmutableContextSlot, kLdaCurrentContextSlot, kLdaImmutableCurrentContextSlot, kStaContextSlot, kStaCurrentContextSlot, kLdaLookupSlot, kLdaLookupContextSlot, kLdaLookupGlobalSlot, kLdaLookupSlotInsideTypeof, kLdaLookupContextSlotInsideTypeof, kLdaLookupGlobalSlotInsideTypeof, kStaLookupSlot, kLdar, kStar, kMov, kLdaNamedProperty, kLdaNamedPropertyNoFeedback, kLdaKeyedProperty, kLdaModuleVariable, kStaModuleVariable, kStaNamedProperty, kStaNamedPropertyNoFeedback, kStaNamedOwnProperty, kStaKeyedProperty, kStaInArrayLiteral, kStaDataPropertyInLiteral, kCollectTypeProfile, kAdd, kSub, kMul, kDiv, kMod, kExp, kBitwiseOr, kBitwiseXor, kBitwiseAnd, kShiftLeft, kShiftRight, kShiftRightLogical, kAddSmi, kSubSmi, kMulSmi, kDivSmi, kModSmi, kExpSmi, kBitwiseOrSmi, kBitwiseXorSmi, kBitwiseAndSmi, kShiftLeftSmi, kShiftRightSmi, kShiftRightLogicalSmi, kInc, kDec, kNegate, kBitwiseNot, kToBooleanLogicalNot, kLogicalNot, kTypeOf, kDeletePropertyStrict, //省略...................
}

V8规定:fb代表寄存器R0,fa代表寄存器R1,以此类推。在29 f9 02 LdaNamedPropertyNoFeedback r2, [2]中,f9代表寄存器R2,02代表常量池[2]。执行LdaNamedPropertyNoFeedback时,Ignition通过Isolate获取dispatch_table的base address,再通过base address+0x29得到LdaNamedPropertyNoFeedback的handler,源码如下:

// Calls the GetProperty builtin for <object> and the key in the accumulator.
IGNITION_HANDLER(LdaNamedPropertyNoFeedback, InterpreterAssembler) {
  TNode<Object> object = LoadRegisterAtOperandIndex(0);
  TNode<Name> name = CAST(LoadConstantPoolEntryAtOperandIndex(1));
  TNode<Context> context = GetContext();
  TNode<Object> result =
      CallBuiltin(Builtins::kGetProperty, context, object, name);
  SetAccumulator(result);
  Dispatch();
}

 

3 Dispatch

Dispatch_table是指针数组,Bytecode的枚举值代表它在数组中的位置,该位置存储了对应的Bytecode handler的地址。Dispatch_table的初始化如下:

1.  void Interpreter::Initialize() {
2.    Builtins* builtins = isolate_->builtins();
3.    // Set the interpreter entry trampoline entry point now that builtins are
4.    // initialized.
5.    Handle<Code> code = BUILTIN_CODE(isolate_, InterpreterEntryTrampoline);
6.    DCHECK(builtins->is_initialized());
7.    DCHECK(code->is_off_heap_trampoline() ||
8.           isolate_->heap()->IsImmovable(*code));
9.    interpreter_entry_trampoline_instruction_start_ = code->InstructionStart();
10.    // Initialize the dispatch table.
11.    Code illegal = builtins->builtin(Builtins::kIllegalHandler);
12.    int builtin_id = Builtins::kFirstBytecodeHandler;
13.    ForEachBytecode([=, &builtin_id](Bytecode bytecode,
14.                                     OperandScale operand_scale) {
15.      Code handler = illegal;
16.      if (Bytecodes::BytecodeHasHandler(bytecode, operand_scale)) {
17.  #ifdef DEBUG
18.        std::string builtin_name(Builtins::name(builtin_id));
19.        std::string expected_name =
20.            Bytecodes::ToString(bytecode, operand_scale, "") + "Handler";
21.        DCHECK_EQ(expected_name, builtin_name);
22.  #endif
23.        handler = builtins->builtin(builtin_id++);
24.      }
25.      SetBytecodeHandler(bytecode, operand_scale, handler);
26.    });
27.    DCHECK(builtin_id == Builtins::builtin_count);
28.    DCHECK(IsDispatchTableInitialized());
29.  }

上述13-26行代码是匿名函数,其中25行代码初始化Dispatch_table,源码如下:

1.  void Interpreter::SetBytecodeHandler(Bytecode bytecode,
2.                                       OperandScale operand_scale, Code handler) {
3.    DCHECK(handler.kind() == Code::BYTECODE_HANDLER);
4.    size_t index = GetDispatchTableIndex(bytecode, operand_scale);
5.    dispatch_table_[index] = handler.InstructionStart();
6.  }
7.  //.........分隔线.............................................
8.  size_t Interpreter::GetDispatchTableIndex(Bytecode bytecode,
9.                                            OperandScale operand_scale) {
10.    static const size_t kEntriesPerOperandScale = 1u << kBitsPerByte;
11.    size_t index = static_cast<size_t>(bytecode);
12.    return index + BytecodeOperands::OperandScaleAsIndex(operand_scale) *
13.                       kEntriesPerOperandScale;
14.  }

上述第5行代码dispatch_table_就是我们念念已久的存储dispatch table的成员变量;第4行代码GetDispatchTableIndex()计算Bytecode handler在dispatch_table中的位置,这个位置与enum class Bytecode是相同的。图1给出了SetBytecodeHandler的调用堆栈。

Interpreter的源码如下:

1.  class Interpreter {
2.      //............省略..................
3.   private:
4.    // Get dispatch table index of bytecode.
5.    static size_t GetDispatchTableIndex(Bytecode bytecode,
6.                                        OperandScale operand_scale);
7.    static const int kNumberOfWideVariants = BytecodeOperands::kOperandScaleCount;
8.    static const int kDispatchTableSize = kNumberOfWideVariants * (kMaxUInt8 + 1);
9.    static const int kNumberOfBytecodes = static_cast<int>(Bytecode::kLast) + 1;
10.    Isolate* isolate_;
11.    Address dispatch_table_[kDispatchTableSize];
12.    std::unique_ptr<uintptr_t[]> bytecode_dispatch_counters_table_;
13.    Address interpreter_entry_trampoline_instruction_start_;
14.    DISALLOW_COPY_AND_ASSIGN(Interpreter);
15.  };

上述第11行代码dispatch_table_Interpreter的成员变量。Interpreter是Isolate的成员变量,源码如下:

1.  class Isolate final : private HiddenFactory {
2.  //省略...............
3.    const AstStringConstants* ast_string_constants_ = nullptr;
4.    interpreter::Interpreter* interpreter_ = nullptr;
5.    compiler::PerIsolateCompilerCache* compiler_cache_ = nullptr;
6.    Zone* compiler_zone_ = nullptr;
7.    CompilerDispatcher* compiler_dispatcher_ = nullptr;
8.    friend class heap::HeapTester;
9.    friend class TestSerializer;
10.    DISALLOW_COPY_AND_ASSIGN(Isolate);
11.  };

通过上述代码可以看出:Isolate->interpreter_->dispatch_table_获取dispatch_table_
下面是在Bytecode handler中调用的Dispatch()的源码:

1.  void InterpreterAssembler::Dispatch() {
2.    Comment("========= Dispatch");
3.    DCHECK_IMPLIES(Bytecodes::MakesCallAlongCriticalPath(bytecode_), made_call_);
4.    TNode<IntPtrT> target_offset = Advance();
5.    TNode<WordT> target_bytecode = LoadBytecode(target_offset);
6.    if (Bytecodes::IsStarLookahead(bytecode_, operand_scale_)) {
7.      target_bytecode = StarDispatchLookahead(target_bytecode);
8.    }
9.    DispatchToBytecode(target_bytecode, BytecodeOffset());
10.   }
11.   //......分隔线....................................
12.   void InterpreterAssembler::DispatchToBytecode(
13.       TNode<WordT> target_bytecode, TNode<IntPtrT> new_bytecode_offset) {
14.     if (FLAG_trace_ignition_dispatches) {
15.       TraceBytecodeDispatch(target_bytecode);
16.     }
17.     TNode<RawPtrT> target_code_entry = Load<RawPtrT>(
18.         DispatchTablePointer(), TimesSystemPointerSize(target_bytecode));
19.     DispatchToBytecodeHandlerEntry(target_code_entry, new_bytecode_offset);
20.   }
21.   //...........分隔线....................................
22.   void InterpreterAssembler::DispatchToBytecodeHandlerEntry(
23.       TNode<RawPtrT> handler_entry, TNode<IntPtrT> bytecode_offset) {
24.     // Propagate speculation poisoning.
25.     TNode<RawPtrT> poisoned_handler_entry =
26.         UncheckedCast<RawPtrT>(WordPoisonOnSpeculation(handler_entry));
27.     TailCallBytecodeDispatch(InterpreterDispatchDescriptor{},
28.                              poisoned_handler_entry, GetAccumulatorUnchecked(),
29.                              bytecode_offset, BytecodeArrayTaggedPointer(),
30.                              DispatchTablePointer());
31.   }
32.    //...........分隔线....................................
33.  void CodeAssembler::TailCallBytecodeDispatch(
34.      const CallInterfaceDescriptor& descriptor, TNode<RawPtrT> target,
35.      TArgs... args) {
36.    DCHECK_EQ(descriptor.GetParameterCount(), sizeof...(args));
37.    auto call_descriptor = Linkage::GetBytecodeDispatchCallDescriptor(
38.        zone(), descriptor, descriptor.GetStackParameterCount());
39.    Node* nodes[] = {target, args...};
40.    CHECK_EQ(descriptor.GetParameterCount() + 1, arraysize(nodes));
41.    raw_assembler()->TailCallN(call_descriptor, arraysize(nodes), nodes);
42.  }

上述三个方法共同实现Bytecode的dispatch。第5行代码计算target_bytecode;第17行代码计算target_bytecode_entry;第27行代码开始跳转;第34行代码创建call discriptor;第41行代码生成Node节点,并把该节点添加到当前基本块的尾部,至此跳转完成。TailCallN()的详细讲解参见第十一篇文章。图2给出了Dispatch()的调用堆栈。

技术总结

(1) Bytecode的编号是Bytecode handler在数组dispatch_table_中的下标;

(2) dispatchtable的初始化在Isolate启动时完成;

(3) 使用固定的物理寄存器保存dispatch_table_的优点是:避免不必要的入栈和出栈,简化Bytecode的设计,提高了Dispatch的效率;

提示:我调试V8时,dispatch_table_始终保存在物理寄存器R15中,调试方法参见第18篇文章。

好了,今天到这里,下次见。

个人能力有限,有不足与纰漏,欢迎批评指正
微信:qq9123013 备注:v8交流 邮箱:v8blink@outlook.com

(完)