Давайте разберемся в Chrome V8 — Глава 11: Диспетчеризация байткода

Первоисточник: https://medium.com/@huidou/lets-understand-chrome-v8-chapter-11-bytecode-dispatch-ab6415e32bbd
Добро пожаловать в другие главы «Давайте поймем Chrome V8».


Dispatch отвечает за планирование байткода, что эквивалентно регистру EIP++ и переходу к следующему байткоду. Диспетчер состоит из двух частей, одна из которых — таблица диспетчеризации, а другая — физический регистр. Таблица — это массив, который содержит все адреса байткода. V8 использует физический регистр для диспетчеризации байткода для достижения большей эффективности.

1. Диспетчерская таблица

Диспетчерская таблица — это массив указателей, который используется для хранения адреса программы обработки байткода. Тип массива — Code, а класс Code приведен ниже:

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.  };
Войти в полноэкранный режим Выйти из полноэкранного режима

Примечание: В изоляторе есть еще один массив указателей, который имеет тот же тип, что и таблица диспетчеризации. В нем хранятся все адреса Builtins, что может легко запутать новичков.

Диспетчерская таблица поддерживается BuildWithMacroAssembler, код приведен ниже:

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.  }
Войти в полноэкранный режим Выйти из полноэкранного режима

Как отлаживать BuildWithMacroAssembler смотрите в главе 9. Когда значение параметра builtin_index равно 65, генератор указателей функций указывает на Generate_InterpreterEnterBytecodeDispatch, который отвечает за ведение таблицы диспетчеризации, код приведен ниже:

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.  }
Вход в полноэкранный режим Выход из полноэкранного режима

В приведенном выше коде Generate_InterpreterEnterBytecodeDispatch является точкой входа в диспетчер байткода, а Generate_InterpreterEnterBytecode является ключевым кодом, реализующим диспетчер байткода. Вот два важных понятия:

  • Generate_InterpreterEnterBytecodeDispatch является Builtin и его индекс равен 65. (Индекс может отличаться в разных версиях V8).

  • V8 использует физический регистр для управления таблицей диспетчеризации, имя регистра — kInterpreterDispatchTableRegister. Использование физического регистра позволяет избежать частого выталкивания и всплывания таблицы диспетчеризации, что повышает эффективность.

Вот ключевые строки кода:

(1) Строка 35, загружает таблицу диспетчеризации в регистр A. Строка 37 кода, извлекает адрес таблицы из изолятора. Реализация A заключается в следующих трех функциях.

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]);
    }
Вход в полноэкранный режим Выход из полноэкранного режима

(2) Строка 39 извлекает массив байткода из стека.

(3) Строка 51 извлекает смещение целевого байткода из массива байткода.

(4) Строки 56 и 58 вычисляют адрес целевого байткода и сохраняют его в регистре kJavaScriptCallCodeStartRegister.

(5) Строка 61 переходит к регистру kJavaScriptCallCodeStartRegister и выполняет байткод. На рисунке 1 представлен стек вызовов.

2. TailCall

Dispatch() является последним шагом в байткоде, поэтому его псевдоним — хвостовой вызов. Давайте рассмотрим несколько байткодов:.

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.   }
Вход в полноэкранный режим Выход из полноэкранного режима

Все три приведенных выше байткода заканчиваются Dispatch(). Ниже приведен код 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. }
Вход в полноэкранный режим Выйти из полноэкранного режима

Вторая строка — это функция комментария. При отладке мы можем использовать ее выходную информацию для определения позиции выполнения. В 4-й строке выводится смещение целевого (следующего) байткода. 5-я строка загружает целевой байткод. 9-я строка вызывает DspatchToBytecode(), которая в конечном итоге вызывает DispatchToBytecodeHandlerEntry().

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());
}
Вход в полноэкранный режим Выход из полноэкранного режима

В DispatchToBytecodeHandlerEntry параметром 1 является целевой байткод, а 2 — смещение байткода. Параметр 1 используется для загрузки обработчика байткода, а 2 — для загрузки операндов. Затем вызовите команду TailCallBytecodeDispatch, которая приведена ниже:

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.  }
Войти в полноэкранный режим Выйти из полноэкранного режима

В приведенном выше коде 5-я строка создает дескриптор, как показано ниже:

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.  }
Войти в полноэкранный режим Выйти из полноэкранного режима

В GetBytecodeDispatchCallDescriptor параметром 2 является целевой байткод, а параметром 3 — количество параметров стека. Эта функция используется для обращения к ресурсам регистра и создания CallDescriptor. CallDescriptor указывает, какие параметры необходимо предоставить при выполнении байткода. В комментариях к строкам 24-25 приведена его схема.

В TailCallBytecodeDispatch, TailCallN() на 9-й строке ниже, мы видим, что параметром 1 является 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.  }
Вход в полноэкранный режим Выход из полноэкранного режима

В 5-й строке генерируется узел. В 7-й строке AddTailCall() добавляет узел в конец текущего основного блока. На этом диспетчеризация завершена и начинается выполнение следующего байткода. На рисунке 2 показан стек вызовов функций.


Итак, на этом мы заканчиваем этот раздел. Увидимся в следующий раз, ребята, всего доброго!

Пожалуйста, свяжитесь со мной, если у вас возникнут какие-либо вопросы. WeChat: qq9123013 Email: v8blink@outlook.com

Оцените статью
devanswers.ru
Добавить комментарий