前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >V8源码入门

V8源码入门

作者头像
车雄生
发布2023-04-26 19:06:52
9600
发布2023-04-26 19:06:52
举报
文章被收录于专栏:咩嗒

本文所用的V8版本为9.4.146.24,源码层面分析builtin、Ignition、Sparkplug、TurboFan。

builtin

builtin是理解V8源码的关键,因为

  • 它本身很重要,是V8最重要的“积木块”;比如ignition解析器每一条指令实现就是一个builtin,js调用原生也是一个builtin,js的很多内置函数(比如Array.prototype.join)也是一个builtin。
  • 它很难懂,因为大多数builtin的“源码”,其实是builtin的生成逻辑

对于第二点,举个例子,很多介绍Ignition的文章会告诉你Ldar指令的实现如下:

代码语言:javascript
复制
IGNITION_HANDLER(Ldar, InterpreterAssembler) {
  TNode<Object> value = LoadRegisterAtOperandIndex(0);
  SetAccumulator(value);
  Dispatch();
}

也确实是,但问题上述代码运行时不会跑,(9.4版本)甚至都不会编译到运行时,这就很让人困惑。

其实上述逻辑只在V8的编译阶段由mksnapshot程序执行,在该进程先通过jit产出机器码,然后dump下来放到汇编文件gen\embedded.S里(在window下会以inline asm放到c++文件gen\http://embedded.cc里),再重新编译到V8库(相当于用jit编译器去AOT)。

上述ldar指令dump到gen\embedded.S后会这样子:

代码语言:javascript
复制
Builtins_LdarHandler:
.def Builtins_LdarHandler; .scl 2; .type 32; .endef;
  .octa 0x72ba0b74d93b48fffffff91d8d48,0xec83481c6ae5894855ccffa9104ae800
  .octa 0x2454894cf0e4834828ec8348e2894920,0x458948e04d894ce87d894cf065894c20
  .octa 0x4d0000494f808b4500001410858b4dd8,0x1640858b49e1894c00000024bac603
  .octa 0x4d00000000158d4ccc01740fc4f64000,0x2045c749d0ff206d8949285589
  .octa 0xe4834828ec8348e289492024648b4800,0x808b4500001410858b4d202454894cf0
  .octa 0x858b49d84d8b48d233c6034d00004953,0x158d4ccc01740fc4f64000001640
  .octa 0x2045c749d0ff206d89492855894d0000,0x5d8b48f0658b4c2024648b4800000000
  .octa 0x4cf7348b48007d8b48011c74be0f49e0,0x100000000ba49211cb60f43024b8d
  .octa 0xa90f4fe800000002ba0b77d33b4c0000,0x8b48006d8b48df0c8b49e87d8b4cccff
  .octa 0xcccccccccccccccc90e1ff30c48348c6
  .byte 0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc

builtin在这文件定义:v8\src\builtins\builtins-definitions.h,这个文件还还include一个根据ignition指令生成的builtin定义以及torque编译器生成的builtin定义,一共1700+个builtin。每个builtin,都在gen\embedded.S那生成一段代码。

builtin的生成

builtin的生成位于这个文件:v8\src\builtins\http://setup-builtins-internal.cc

代码语言:javascript
复制
void SetupIsolateDelegate::SetupBuiltinsInternal(Isolate* isolate) {
  Builtins* builtins = isolate->builtins();
  DCHECK(!builtins->initialized_);

  PopulateWithPlaceholders(isolate);

  // Create a scope for the handles in the builtins.
  HandleScope scope(isolate);

  int index = 0;
  Code code;
#define BUILD_CPP(Name)                                      \
  code = BuildAdaptor(isolate, Builtin::k##Name,             \
                      FUNCTION_ADDR(Builtin_##Name), #Name); \
  AddBuiltin(builtins, Builtin::k##Name, code);              \
  index++;

#define BUILD_TFJ(Name, Argc, ...)                                         \
  code = BuildWithCodeStubAssemblerJS(                                     \
      isolate, Builtin::k##Name, &Builtins::Generate_##Name, Argc, #Name); \
  AddBuiltin(builtins, Builtin::k##Name, code);                            \
  index++;

#define BUILD_TFC(Name, InterfaceDescriptor)                      \
  /* Return size is from the provided CallInterfaceDescriptor. */ \
  code = BuildWithCodeStubAssemblerCS(                            \
      isolate, Builtin::k##Name, &Builtins::Generate_##Name,      \
      CallDescriptors::InterfaceDescriptor, #Name);               \
  AddBuiltin(builtins, Builtin::k##Name, code);                   \
  index++;

#define BUILD_TFS(Name, ...)                                            \
  /* Return size for generic TF builtins (stub linkage) is always 1. */ \
  code = BuildWithCodeStubAssemblerCS(isolate, Builtin::k##Name,        \
                                      &Builtins::Generate_##Name,       \
                                      CallDescriptors::Name, #Name);    \
  AddBuiltin(builtins, Builtin::k##Name, code);                         \
  index++;

#define BUILD_TFH(Name, InterfaceDescriptor)                 \
  /* Return size for IC builtins/handlers is always 1. */    \
  code = BuildWithCodeStubAssemblerCS(                       \
      isolate, Builtin::k##Name, &Builtins::Generate_##Name, \
      CallDescriptors::InterfaceDescriptor, #Name);          \
  AddBuiltin(builtins, Builtin::k##Name, code);              \
  index++;

#define BUILD_BCH(Name, OperandScale, Bytecode)                           \
  code = GenerateBytecodeHandler(isolate, Builtin::k##Name, OperandScale, \
                                 Bytecode);                               \
  AddBuiltin(builtins, Builtin::k##Name, code);                           \
  index++;

#define BUILD_ASM(Name, InterfaceDescriptor)                        \
  code = BuildWithMacroAssembler(isolate, Builtin::k##Name,         \
                                 Builtins::Generate_##Name, #Name); \
  AddBuiltin(builtins, Builtin::k##Name, code);                     \
  index++;

  BUILTIN_LIST(BUILD_CPP, BUILD_TFJ, BUILD_TFC, BUILD_TFS, BUILD_TFH, BUILD_BCH,
               BUILD_ASM);

#undef BUILD_CPP
#undef BUILD_TFJ
#undef BUILD_TFC
#undef BUILD_TFS
#undef BUILD_TFH
#undef BUILD_BCH
#undef BUILD_ASM

  // ...
}

BUILTIN_LIST宏内定义了所有的builtin,并根据其类型去调用不同的参数,在这里参数是BUILD_CPP, BUILD_TFJ...这些,定义了不同的生成策略,这些参数去掉前缀代表不同的builtin类型(CPP, TFJ, TFC, TFS, TFH, BCH, ASM)

builtin的生成逻辑有两种:

  • 直接生成机器码,ASM和CPP类型builtin使用这种方式(CPP类型只是生成适配器,下面会详细说)
  • 先生成turbofan的graph(IR),然后由turbofan编译器编译到机器码,除ASM和CPP之外其它builtin类型都是这种

builtin如何dump到gen\embedded.S

要找到这段逻辑比较简单,gen\embedded.S开头有些注释,比如这段:Autogenerated file. Do not edit, 你在V8源码搜索这段文字即可,这段dump逻辑比较简单,这里就不再赘述。

ASM类型builtin

找了个比较简单ASM类型builtin:DoubleToI,功能是把double转成整数,该builtin的jit逻辑位于Builtins::Generate_DoubleToI,如果是x64的window,该函数放在http://builtins-x64.cc文件。由于每个CPU架构的指令都不一样,所以每个CPU架构都有一个实现,放在各自的http://builtins-ArchName.cc文件。

x64的实现如下:

代码语言:javascript
复制
void Builtins::Generate_DoubleToI(MacroAssembler* masm) {
  Label check_negative, process_64_bits, done;

  // Account for return address and saved regs.
  const int kArgumentOffset = 4 * kSystemPointerSize;

  MemOperand mantissa_operand(MemOperand(rsp, kArgumentOffset));
  MemOperand exponent_operand(
      MemOperand(rsp, kArgumentOffset + kDoubleSize / 2));

  // The result is returned on the stack.
  MemOperand return_operand = mantissa_operand;

  Register scratch1 = rbx;

  // Since we must use rcx for shifts below, use some other register (rax)
  // to calculate the result if ecx is the requested return register.
  Register result_reg = rax;
  // Save ecx if it isn't the return register and therefore volatile, or if it
  // is the return register, then save the temp register we use in its stead
  // for the result.
  Register save_reg = rax;
  __ pushq(rcx);
  __ pushq(scratch1);
  __ pushq(save_reg);

  __ movl(scratch1, mantissa_operand);
  __ Movsd(kScratchDoubleReg, mantissa_operand);
  __ movl(rcx, exponent_operand);

  __ andl(rcx, Immediate(HeapNumber::kExponentMask));
  __ shrl(rcx, Immediate(HeapNumber::kExponentShift));
  __ leal(result_reg, MemOperand(rcx, -HeapNumber::kExponentBias));
  __ cmpl(result_reg, Immediate(HeapNumber::kMantissaBits));
  __ j(below, &process_64_bits, Label::kNear);

  // Result is entirely in lower 32-bits of mantissa
  int delta =
      HeapNumber::kExponentBias + base::Double::kPhysicalSignificandSize;
  __ subl(rcx, Immediate(delta));
  __ xorl(result_reg, result_reg);
  __ cmpl(rcx, Immediate(31));
  __ j(above, &done, Label::kNear);
  __ shll_cl(scratch1);
  __ jmp(&check_negative, Label::kNear);

  __ bind(&process_64_bits);
  __ Cvttsd2siq(result_reg, kScratchDoubleReg);
  __ jmp(&done, Label::kNear);

  // If the double was negative, negate the integer result.
  __ bind(&check_negative);
  __ movl(result_reg, scratch1);
  __ negl(result_reg);
  __ cmpl(exponent_operand, Immediate(0));
  __ cmovl(greater, result_reg, scratch1);

  // Restore registers
  __ bind(&done);
  __ movl(return_operand, result_reg);
  __ popq(save_reg);
  __ popq(scratch1);
  __ popq(rcx);
  __ ret(0);
}

看上去很像汇编(编程的思考方式按汇编来),实际上是c++函数,比如这行movl

代码语言:javascript
复制
__ movl(scratch1, mantissa_operand);

__是个宏,实际上是调用masm变量的函数(movl)

代码语言:javascript
复制
#define __ ACCESS_MASM(masm) 
#define ACCESS_MASM(masm) masm->

而movl的实现是往pc_指针指向的内存写入mov指令及其操作数,并把pc_指针前进指令长度。

ps:一条条指令写下来,然后把内存权限改为可执行,这就是jit的基本原理。

比较有意思的是往后面的指令跳转的实现,比如这行:

代码语言:javascript
复制
__ jmp(&check_negative, Label::kNear);

调用jmp时目标指令的offset还未知呢,于是先在Label记录下这个跳转指令,如果一个Lable在bind前有多个跳转,会利用跳转指令的待定操作数串成一个链表,当调用bind的时候,回填这些待定操作数。

CPP类型builtin

CPP类型builtin使用“真.C++”编写,不过在SetupBuiltinsInternal那仍然要生成一个适配器放到gen\embedded.S。

如下是x86的适配器生成逻辑,十分简单,先加载C++函数地址到kJavaScriptCallExtraArg1Register,然后跳转TFC类型的builtin:AdaptorWithBuiltinExitFrame

代码语言:javascript
复制
void Builtins::Generate_Adaptor(MacroAssembler* masm, Address address) {
  __ LoadAddress(kJavaScriptCallExtraArg1Register,
                 ExternalReference::Create(address));
  __ Jump(BUILTIN_CODE(masm->isolate(), AdaptorWithBuiltinExitFrame),
          RelocInfo::CODE_TARGET);
}

AdaptorWithBuiltinExitFrame会检查参数,然后调用一个ASM类型的builtin:CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit,CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit主要是取参数按各平台的ABI去调用C++实现的builtin逻辑。

HandleApiCall是实现FunctionTemplate原生扩展的关键,就是一个CPP类型builtin

代码语言:javascript
复制
BUILTIN(HandleApiCall) {
  HandleScope scope(isolate);
  Handle<JSFunction> function = args.target();
  Handle<Object> receiver = args.receiver();
  Handle<HeapObject> new_target = args.new_target();
  Handle<FunctionTemplateInfo> fun_data(function->shared().get_api_func_data(),
                                        isolate);
  if (new_target->IsJSReceiver()) {
    RETURN_RESULT_OR_FAILURE(
        isolate, HandleApiCallHelper<true>(isolate, function, new_target,
                                           fun_data, receiver, args));
  } else {
    RETURN_RESULT_OR_FAILURE(
        isolate, HandleApiCallHelper<false>(isolate, function, new_target,
                                            fun_data, receiver, args));
  }
}

BUILTIN是一个宏,定义了上述逻辑的对外接口函数。对外接口函数会被CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit调用。

其它类型builtin

除了ASM和CPP的其它类型builtin都通过调用CodeStubAssembler API(下称CSA)编写,这套API和之前介绍ASM类型builtin时提到的“类汇编API”类似,不同的是“类汇编API”直接产出原生代码,CSA产出的是turbofan的graph(IR)。

CSA比起“类汇编API”的好处是不用每个平台各写一次。

尽管如此,和汇编类似的CSA还是太低级了,写起来太废功夫了,于是V8提供了一个类javascript的高级语言:torque ,这语言最终会编译成CSA形式的c++代码和V8其它C++代码一起编译。

Ignition

Ignition的指令

指令的定义位于:v8\src\interpreter\bytecodes.h

指令的实现(生成逻辑)位于:v8\src\interpreter\http://interpreter-generator.cc

以最简单的LdaZero指令为例:

代码语言:javascript
复制
IGNITION_HANDLER(LdaZero, InterpreterAssembler) {
  TNode<Number> zero_value = NumberConstant(0.0);
  SetAccumulator(zero_value); //把0设置倒累加器
  Dispatch();                 //执行下一条指令
}

NumberConstant,SetAccumulator,Dispatch都是基于CSA封装,本质上也是CSA。

再次强调下CSA并不是真正的操作,可以认为这是LdaZero指令的生成器,实际Ignition虚拟机运行的时候不会跑到这段逻辑。

我们可以断点验证下:

  • 设置mksnapshot为启动项目
  • 断点在上面的LdaZero指令生成逻辑的第一行

断点触发后,再到turbofan graph节点创建的地方(位于v8\src\compiler\http://node.cc的Node::NewImpl函数)下断点。然后运行,然后我们可以观测到这小段代码执行过程中的graph节点创建。

Ignition的运行

指令间的衔接

Ignition的指令间的衔接看上述Dispatch的逻辑

  • DispatchTable是个数组:DispatchTable[Bytecode]即是Bytecode指令的builtin原生代码入口
  • 取DispatchTable[Next-Bytecode]后,尾调用过去

ps:一个解析器的实现并不复杂,定义好指令和相应的操作,然后某种方式(比如lua的while + switch)一条条指令执行相应的操作即可。

函数入口

一个函数的第一个指令的builtin并不是第一个执行的builtin,因为还需要诸如一些当前函数帧初始化的操作,这些操作在这几个这几个ASM类型的builtin里:JSEntry、JSEntryTrampoline、InterpreterEntryTrampoline

Sparkplug

Sparkplug是v9.1加入的(jit)编译器,在此之前已经有一个(jit)编译器TurboFan了,为啥还要加一个呢?它们的区别是什么呢?让我们带着这疑问往下看

断点

代码语言:javascript
复制
void BaselineCompiler::VisitSingleBytecode()

ps:类名为什么叫BaselineCompiler而不是SparkplugCompiler?不知道,但整个http://baseline-compiler.cc是被#if ENABLE_SPARKPLUG宏框住的,这块应该是Sparkplug的实现。

在d8窗口输入如下代码可以触发Sparkplug编译

代码语言:javascript
复制
function add(x, y) {return x + y}
for(var i = 0; i< 100;i++) add(1, 2) //循环调用add,直到触发Sparkplug
for(var i = 0; i< 100;i++) add(1, 2)
for(var i = 0; i< 100;i++) add(1, 2)
for(var i = 0; i< 100;i++) add(1, 2)
for(var i = 0; i< 100;i++) add(1, 2)
for(var i = 0; i< 100;i++) add(1, 2) //触发Sparkplug

BaselineCompiler::VisitSingleBytecode针对每条Ignition指令用宏生成switch-case,每个case根据Ignition指令调用对应的Visit函数,Visit函数里会生成对应builtin的调用(对于每一个Ignition指令,在Sparkplug有对应的另外一个builtin)。

以kAdd为例,调用的是VisitAdd,而VisitAdd会生成对TFC类型的Add_Baseline builtin的调用。

Add_Baseline builtin的生成逻辑在v8\src\builtins\http://builtins-number-gen.cc的63行

对比Ignition的builtin的生成逻辑(v8\src\interpreter\http://interpreter-generator.cc的870行),它们俩部分逻辑是重用的,核心区别是Ignition最后面是Dispatch,尾调用下一条指令,而Sparkplug则是return。

结论:

  • Sparkplug比较简单粗暴:针对ignition指令(jit)生成一个个call指令,调用对应的builtin,相比ignition,省掉了加载指令地址然后尾调用的过程
  • 好处是没有优化过程,所以编译开销小
  • 坏处也是没有优化过程,生成的代码比turbofan性能差

所以Sparkplug的意图是在Ignition和TurboFan间加入一个更中庸的方案,它编译开销比TurboFan开销小,还不是十分热点的地方也可以用,而TurboFan编译出来的代码跑得快,但它编译的开销大,所以更热点的地方才执行TurboFan编译。

TurboFan

builtin是V8实现的重要积木块,而这些积木块大多是TurboFan编译的,包括Ignition和Sparkplug的指令实现也是用TurboFan编译的builtin。同时TurboFan也是V8高性能的关键,其重要性不言而喻。

本文只是简单讲下个整体的处理框架:一次TurboFan编译抽象为一个Pipeline,Pipeline有一个个Phase,这些Phase大致分为四部分:

  • Graph(IR)生成:这Phase有个遍历Ignition指令,生成Graph的过程
  • 对Graph优化,比如inline,死代码消除之类
  • 优化后的Graph转CPU架构相关的中间指令
  • 上一步的中间指令转原生代码

Ignition指令 -> TurboFan Graph Node

在创建节点处断点 F:\v8\v8\src\compiler\http://node.cc

代码语言:javascript
复制
template <typename NodePtrT>
Node* Node::NewImpl(Zone* zone, NodeId id, const Operator* op, int input_count,
                    NodePtrT const* inputs, bool has_extensible_inputs)

如下js代码可以触发TurboFan编译

代码语言:javascript
复制
function add(x, y) {return x + y} 
for(var i = 0; i< 10000000;i++) add(1, 2)

ps:触发了Sparkplug,上述代码仍然会触发TurboFan编译,但如果没之前的小规模调用,一上来就10000000循环,会直接触发TurboFan,不会再触发Sparkplug。

断点触发后可以看到创建TurboFan Graph的逻辑在BytecodeGraphBuilder::CreateGraph

里面对VisitBytecodes就是根据Ignition指令创建Graph对应的节点的地方。这块和Sparkplug是类似的。

Graph优化

前一章的断点堆栈上,BytecodeGraphBuilder::CreateGraph往前还有一个PipelineImpl::CreateGraph栈帧。

通过PipelineImpl::CreateGraph的代码可以看到Ignition指令转TurboFan Graph Node是其中一个Phase:GraphBuilderPhase,还有和优化相关的Phase,比如InliningPhase,而更多优化Phase在这里:PipelineImpl::OptimizeGraph

TurboFan Graph -> 中间指令(ArchOpcode)

优化完毕的Graph,不会直接转原生代码,而是先转到一个更底层的,和CPU架构相关的指令。我们还是通过断点来观测这个行为。

去掉前面的断点,在v8\src\compiler\backend\http://instruction-selector.cc如下代码下断点,点击“继续”

代码语言:javascript
复制
Instruction* InstructionSelector::Emit(Instruction* instr) {
  instructions_.push_back(instr);
  return instr;
}

主要的转换流程在InstructionSelector::SelectInstructions,也不难懂,这就不展开说了。

中间指令(ArchOpcode)-> 原生代码

核心逻辑在CodeGenerator::AssembleArchInstruction,选择一个常用的OpCode下断点

去掉前面的断点,在v8\src\compiler\backend\x64\http://code-generator-x64.cc如下代码下断点,点击“继续”

代码语言:javascript
复制
CodeGenerator::CodeGenResult CodeGenerator::AssembleArchInstruction(
    Instruction* instr) {
    // ...
    case kArchRet:
      AssembleReturn(instr->InputAt(0));
      break;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-04-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • builtin
    • builtin的生成
      • builtin如何dump到gen\embedded.S
        • ASM类型builtin
          • CPP类型builtin
            • 其它类型builtin
            • Ignition
              • Ignition的指令
                • Ignition的运行
                  • 指令间的衔接
                    • 函数入口
                    • Sparkplug
                    • TurboFan
                      • Ignition指令 -> TurboFan Graph Node
                        • Graph优化
                          • TurboFan Graph -> 中间指令(ArchOpcode)
                            • 中间指令(ArchOpcode)-> 原生代码
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档