DEV Community

Cover image for Let’s Understand Chrome V8 — Chapter 11: Bytecode Dispatch
灰豆
灰豆

Posted on • Originally published at Medium

Let’s Understand Chrome V8 — Chapter 11: Bytecode Dispatch

Original source: https://medium.com/@huidou/lets-understand-chrome-v8-chapter-11-bytecode-dispatch-ab6415e32bbd
Welcome to other chapters of Let’s Understand Chrome V8


Dispatch is responsible for scheduling bytecode, which is equivalent to register EIP++ and jumps to the next bytecode. Dispatch consists of two parts, one is a dispatch table and the other is the physical register. The table is an array that contains all bytecode addresses. V8 uses the physical register to dispatch bytecode for getting greater efficiency.

1. Dispatch Table

The dispatch table is an array of pointers, which is used to store the address of the bytecode processing program. The array type is Code, and the Code Class is below:

1.  class Code : public HeapObject {
2.   public:
3.    NEVER_READ_ONLY_SPACE
4.    using Flags = uint32_t;
5.  #define CODE_KIND_LIST(V)   \
6.    V(OPTIMIZED_FUNCTION)     \
7.    V(BYTECODE_HANDLER)       \
8.    V(STUB)                   \
9.    V(BUILTIN)                \
10.   V(REGEXP)                 \
11.    V(WASM_FUNCTION)          \
12.    V(WASM_TO_CAPI_FUNCTION)  \
13.    V(WASM_TO_JS_FUNCTION)    \
14.    V(JS_TO_WASM_FUNCTION)    \
15.    V(JS_TO_JS_FUNCTION)      \
16.    V(WASM_INTERPRETER_ENTRY) \
17.    V(C_WASM_ENTRY)
18.    enum Kind {
19.  #define DEFINE_CODE_KIND_ENUM(name) name,
20.      CODE_KIND_LIST(DEFINE_CODE_KIND_ENUM)
21.  #undef DEFINE_CODE_KIND_ENUM
22.          NUMBER_OF_KINDS
23.    };
24.    static const char* Kind2String(Kind kind);
25.  #ifdef ENABLE_DISASSEMBLER
26.    const char* GetName(Isolate* isolate) const;
27.    V8_EXPORT_PRIVATE void Disassemble(const char* name, std::ostream& os,
28.                                       Address current_pc = kNullAddress);
29.  #endif
30.  //................omit................
31.  };
Enter fullscreen mode Exit fullscreen mode

Note: In the isolate, there is another pointer array that is the same type as the dispatch table. It holds all the Builtins addresses, which can confuse beginners easily.

The dispatch table is maintained by BuildWithMacroAssembler, code is below:

1.  Code BuildWithMacroAssembler(Isolate* isolate, int32_t builtin_index,
2.                               MacroAssemblerGenerator generator,
3.                               const char* s_name) {
4.    HandleScope scope(isolate);
5.    // Canonicalize handles, so that we can share constant pool entries pointing
6.    // to code targets without dereferencing their handles.
7.    CanonicalHandleScope canonical(isolate);
8.    constexpr int kBufferSize = 32 * KB;
9.    byte buffer[kBufferSize];
10.   MacroAssembler masm(isolate, BuiltinAssemblerOptions(isolate, builtin_index),
11.                        CodeObjectRequired::kYes,
12.                        ExternalAssemblerBuffer(buffer, kBufferSize));
13.    masm.set_builtin_index(builtin_index);
14.    DCHECK(!masm.has_frame());
15.    generator(&masm);
16.    int handler_table_offset = 0;
17.    // JSEntry builtins are a special case and need to generate a handler table.
18.    DCHECK_EQ(Builtins::KindOf(Builtins::kJSEntry), Builtins::ASM);
19.    DCHECK_EQ(Builtins::KindOf(Builtins::kJSConstructEntry), Builtins::ASM);
20.    DCHECK_EQ(Builtins::KindOf(Builtins::kJSRunMicrotasksEntry), Builtins::ASM);
21.    if (Builtins::IsJSEntryVariant(builtin_index)) {
22.      handler_table_offset = HandlerTable::EmitReturnTableStart(&masm);
23.      HandlerTable::EmitReturnEntry(
24.          &masm, 0, isolate->builtins()->js_entry_handler_offset());
25.    }
26.    //.....................................................
27.    //................omit.............................
28.  }
Enter fullscreen mode Exit fullscreen mode

You can see Chapter 9 regarding how to debug the BuildWithMacroAssembler. When the value of the parameter builtin_index is 65, the function pointer generator points to Generate_InterpreterEnterBytecodeDispatch which is responsible for maintaining the Dispatch table, code is below:

1.  static void Generate_InterpreterEnterBytecode(MacroAssembler* masm) {
2.    // Set the return address to the correct point in the interpreter entry
3.    // trampoline.
4.    Label builtin_trampoline, trampoline_loaded;
5.    Smi interpreter_entry_return_pc_offset(
6.        masm->isolate()->heap()->interpreter_entry_return_pc_offset());
7.    DCHECK_NE(interpreter_entry_return_pc_offset, Smi::kZero);
8.    // If the SFI function_data is an InterpreterData, the function will have a
9.    // custom copy of the interpreter entry trampoline for profiling. If so,
10.   // get the custom trampoline, otherwise grab the entry address of the global
11.    // trampoline.
12.    __ movq(rbx, Operand(rbp, StandardFrameConstants::kFunctionOffset));
13.    __ LoadTaggedPointerField(
14.        rbx, FieldOperand(rbx, JSFunction::kSharedFunctionInfoOffset));
15.    __ LoadTaggedPointerField(
16.        rbx, FieldOperand(rbx, SharedFunctionInfo::kFunctionDataOffset));
17.    __ CmpObjectType(rbx, INTERPRETER_DATA_TYPE, kScratchRegister);
18.    __ j(not_equal, &builtin_trampoline, Label::kNear);
19.    __ movq(rbx,
20.            FieldOperand(rbx, InterpreterData::kInterpreterTrampolineOffset));
21.    __ addq(rbx, Immediate(Code::kHeaderSize - kHeapObjectTag));
22.    __ jmp(&trampoline_loaded, Label::kNear);
23.    __ bind(&builtin_trampoline);
24.    // TODO(jgruber): Replace this by a lookup in the builtin entry table.
25.    __ movq(rbx,
26.            __ ExternalReferenceAsOperand(
27.                ExternalReference::
28.                    address_of_interpreter_entry_trampoline_instruction_start(
29.                        masm->isolate()),
30.                kScratchRegister));
31.    __ bind(&trampoline_loaded);
32.    __ addq(rbx, Immediate(interpreter_entry_return_pc_offset.value()));
33.    __ Push(rbx);
34.    // Initialize dispatch table register.
35.    __ Move(
36.        kInterpreterDispatchTableRegister,
37.        ExternalReference::interpreter_dispatch_table_address(masm->isolate()));
38.    // Get the bytecode array pointer from the frame.
39.    __ movq(kInterpreterBytecodeArrayRegister,
40.            Operand(rbp, InterpreterFrameConstants::kBytecodeArrayFromFp));
41.    if (FLAG_debug_code) {
42.      // Check function data field is actually a BytecodeArray object.
43.      __ AssertNotSmi(kInterpreterBytecodeArrayRegister);
44.      __ CmpObjectType(kInterpreterBytecodeArrayRegister, BYTECODE_ARRAY_TYPE,
45.                       rbx);
46.      __ Assert(
47.          equal,
48.          AbortReason::kFunctionDataShouldBeBytecodeArrayOnInterpreterEntry);
49.    }
50.    // Get the target bytecode offset from the frame.
51.    __ movq(kInterpreterBytecodeOffsetRegister,
52.            Operand(rbp, InterpreterFrameConstants::kBytecodeOffsetFromFp));
53.    __ SmiUntag(kInterpreterBytecodeOffsetRegister,
54.                kInterpreterBytecodeOffsetRegister);
55.    // Dispatch to the target bytecode.
56.    __ movzxbq(r11, Operand(kInterpreterBytecodeArrayRegister,
57.                            kInterpreterBytecodeOffsetRegister, times_1, 0));
58.    __ movq(kJavaScriptCallCodeStartRegister,
59.            Operand(kInterpreterDispatchTableRegister, r11,
60.                    times_system_pointer_size, 0));
61.    __ jmp(kJavaScriptCallCodeStartRegister);
62.  }
63.  //================================separation=========================
64.  void Builtins::Generate_InterpreterEnterBytecodeDispatch(MacroAssembler* masm) {
65.    Generate_InterpreterEnterBytecode(masm);
66.  }
Enter fullscreen mode Exit fullscreen mode

In the above code, the Generate_InterpreterEnterBytecodeDispatch is the entry point of bytecode dispatch, and the Generate_InterpreterEnterBytecode is the key code that implements the bytecode dispatch. Here are two important concepts:

  • The Generate_InterpreterEnterBytecodeDispatch is a Builtin and its index is 65. (The index may be dissimilar in different V8 versions)

  • V8 uses a physical register to manage the dispatch table, the name of the register is kInterpreterDispatchTableRegister. Using the physical register can avoid the push and pop of the dispatch table frequently, to improve efficiency.

Here are the key lines of code:

(1) Line 35, loads the dispatch table to the A register. Line 37 of code, take out the table address from the isolate. The implementation of A is in the below three functions.

ExternalReference ExternalReference::interpreter_dispatch_table_address(
     Isolate* isolate) {
   return ExternalReference(isolate->interpreter()->dispatch_table_address());
 }
interpreter::Interpreter* interpreter() const {
     return interpreter_;
   }
Address dispatch_table_address() {
      return reinterpret_cast<Address>(&dispatch_table_[0]);
    }
Enter fullscreen mode Exit fullscreen mode

(2) Line 39 takes out the bytecode array from the stack.

(3) Line 51 takes out the offset of the target bytecode from the bytecode array.

(4) Lines 56 and 58 calculate the target bytecode address and store the address in kJavaScriptCallCodeStartRegister.

(5) Line 61 jumps to register kJavaScriptCallCodeStartRegister and executes bytecode. Figure 1 is the call stack.

Image description

2. TailCall

The Dispatch() is the last step in a bytecode, so its alias is the tail call. Let’s look at a few bytecodes:

1.  IGNITION_HANDLER(StaGlobal, InterpreterAssembler) {
2.    TNode<Context> context = GetContext();
3.  //omit..............
4.     Goto(&end);
5.     Bind(&no_feedback);
6.     CallRuntime(Runtime::kStoreGlobalICNoFeedback_Miss, context, value, name);
7.     Goto(&end);
8.     Bind(&end);
9.     Dispatch();// !!! this is the tail call.
10.   }
11.   IGNITION_HANDLER(LdaContextSlot, InterpreterAssembler) {
12.     TNode<Context> context = CAST(LoadRegisterAtOperandIndex(0));
13.     TNode<IntPtrT> slot_index = Signed(BytecodeOperandIdx(1));
14.     TNode<Uint32T> depth = BytecodeOperandUImm(2);
15.     TNode<Context> slot_context = GetContextAtDepth(context, depth);
16.     TNode<Object> result = LoadContextElement(slot_context, slot_index);
17.     SetAccumulator(result);
18.     Dispatch(); //!!! here also is.
19.   }
20.   IGNITION_HANDLER(LdaImmutableContextSlot, InterpreterAssembler) {
21.     TNode<Context> context = CAST(LoadRegisterAtOperandIndex(0));
22.     TNode<IntPtrT> slot_index = Signed(BytecodeOperandIdx(1));
23.     TNode<Uint32T> depth = BytecodeOperandUImm(2);
24.     TNode<Context> slot_context = GetContextAtDepth(context, depth);
25.     TNode<Object> result = LoadContextElement(slot_context, slot_index);
26.     SetAccumulator(result);
27.     Dispatch(); //!!! here 
28.   }
Enter fullscreen mode Exit fullscreen mode

The above three bytecodes all end with Dispatch(). Below is the code of 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. }
Enter fullscreen mode Exit fullscreen mode

The 2nd line is the comment function. When debugging, we can use its output information to locate the execution position. The 4th line takes out the offset of the target(next) bytecode. The 5th line loads the target bytecode. The 9th line calls DspatchToBytecode() which calls DispatchToBytecodeHandlerEntry() eventually.

void InterpreterAssembler::DispatchToBytecodeHandlerEntry(
    TNode<RawPtrT> handler_entry, TNode<IntPtrT> bytecode_offset) {
  // Propagate speculation poisoning.
  TNode<RawPtrT> poisoned_handler_entry =
      UncheckedCast<RawPtrT>(WordPoisonOnSpeculation(handler_entry));
  TailCallBytecodeDispatch(InterpreterDispatchDescriptor{},
                           poisoned_handler_entry, GetAccumulatorUnchecked(),
                           bytecode_offset, BytecodeArrayTaggedPointer(),
                           DispatchTablePointer());
}
Enter fullscreen mode Exit fullscreen mode

In DispatchToBytecodeHandlerEntry, parameter 1 is the target bytecode and 2 is the offset of the bytecode. Parameter 1 is used to load the bytecode handler and 2 is used to load operands. Then call TailCallBytecodeDispatch, which is below:

0.  template <class... TArgs>
1.  void CodeAssembler::TailCallBytecodeDispatch(
2.      const CallInterfaceDescriptor& descriptor, TNode<RawPtrT> target,
3.      TArgs... args) {
4.    DCHECK_EQ(descriptor.GetParameterCount(), sizeof...(args));
5.    auto call_descriptor = Linkage::GetBytecodeDispatchCallDescriptor(
6.        zone(), descriptor, descriptor.GetStackParameterCount());
7.    Node* nodes[] = {target, args...};
8.    CHECK_EQ(descriptor.GetParameterCount() + 1, arraysize(nodes));
9.    raw_assembler()->TailCallN(call_descriptor, arraysize(nodes), nodes);
10.  }
Enter fullscreen mode Exit fullscreen mode

In the above code, the 5th line creates the descriptor, as below:

0.  CallDescriptor* Linkage::GetBytecodeDispatchCallDescriptor(
1.      Zone* zone, const CallInterfaceDescriptor& descriptor,
2.      int stack_parameter_count) {
3.    const int register_parameter_count = descriptor.GetRegisterParameterCount();
4.    const int parameter_count = register_parameter_count + stack_parameter_count;
5.    DCHECK_EQ(descriptor.GetReturnCount(), 1);
6.    LocationSignature::Builder locations(zone, 1, parameter_count);
7.    locations.AddReturn(regloc(kReturnRegister0, descriptor.GetReturnType(0)));
8.    for (int i = 0; i < parameter_count; i++) {
9.      if (i < register_parameter_count) {
10.        // The first parameters go in registers.
11.        Register reg = descriptor.GetRegisterParameter(i);
12.        MachineType type = descriptor.GetParameterType(i);
13.        locations.AddParam(regloc(reg, type));
14.      } else {
15.        int stack_slot = i - register_parameter_count - stack_parameter_count;
16.        locations.AddParam(LinkageLocation::ForCallerFrameSlot(
17.            stack_slot, MachineType::AnyTagged()));
18.      }
19.    }
20.    MachineType target_type = MachineType::Pointer();
21.    LinkageLocation target_loc = LinkageLocation::ForAnyRegister(target_type);
22.    const CallDescriptor::Flags kFlags =
23.        CallDescriptor::kCanUseRoots | CallDescriptor::kFixedTargetRegister;
24.    return new (zone) CallDescriptor(  // --
25.        CallDescriptor::kCallAddress,  // kind
26.        target_type,                   // target MachineType
27.        target_loc,                    // target location
28.        locations.Build(),             // location_sig
29.        stack_parameter_count,         // stack_parameter_count
30.        Operator::kNoProperties,       // properties
31.        kNoCalleeSaved,                // callee-saved registers
32.        kNoCalleeSaved,                // callee-saved fp
33.        kFlags,                        // flags
34.        descriptor.DebugName());
35.  }
Enter fullscreen mode Exit fullscreen mode

In GetBytecodeDispatchCallDescriptor, parameter 2 is the target bytecode, and parameter 3 is the stack parameter count. This function is used to apply for register resources and create a CallDescriptor. The CallDescriptor gives what parameters to supply when executing a bytecode. The comments on lines 24 to 25 give its layout.

In TailCallBytecodeDispatch, the TailCallN() on the 9th line is below, we see that it’s parameter 1 is CallDescriptor.

1.  void RawMachineAssembler::TailCallN(CallDescriptor* call_descriptor,
2.                                      int input_count, Node* const* inputs) {
3.    // +1 is for target.
4.    DCHECK_EQ(input_count, call_descriptor->ParameterCount() + 1);
5.    Node* tail_call =
6.        MakeNode(common()->TailCall(call_descriptor), input_count, inputs);
7.    schedule()->AddTailCall(CurrentBlock(), tail_call);
8.    current_block_ = nullptr;
9.  }
Enter fullscreen mode Exit fullscreen mode

The 5th line generates a node. In the 7th line AddTailCall() adds the node to the end of the current basic block. So far, the dispatch is completed and the next bytecode is started to execute. Figure 2 shows the function call stack.

Image description


Okay, that wraps it up for this share. I’ll see you guys next time, take care!

Please reach out to me if you have any issues. WeChat: qq9123013 Email: v8blink@outlook.com

Top comments (0)