v8在使用TurboFan引擎进行代码的优化处理时需要先将代码转换成由节点组成的图(Graph),在这之后的各优化处理阶段都会对该图中的节点进行优化处理。GraphBuilder阶段是优化处理时第一个被执行的阶段,此阶段负责生成并初始化Graph,以以下代码为例:
function opt(){
let i = 0;
return i;
}
//优化
for(let i = 0; i < 0x3000; i++){
opt();
}
opt();
在查看其节点图之前需要注意的的是优化引擎对于函数的处理,通常情况下优化引擎会将函数分为两种,main function与inlined function,inlined函数会被内联到main函数中被视为是main函数的一部分:
查看根据代码生成的节点图:
由于节点较多,只截取说明for循环部分,当执行循环时23.phi节点用于存储i变量的值,随后将23与48节点(常量1)传入49节点进行加操作,49节点会成为23、51节点的输入,成为23节点的输入是为了将加后的结果再赋值给变量i对应代码中的i++,51节点通常会是FrameState节点的输入,FrameState节点按照我的理解就是用于采集栈信息用于在发生解优化或其他需要回溯或获取栈信息时使用,FrameState节点所要保存的值通常会通过StateValue节点传入;除此之外49节点还会成为50节点的效果输入节点,50节点有两个用途,其一是将其输出结果作为值输入给20节点进行循环,其二是将其作为效果链EffectPhi节点的效果输入节点用于栈检查。当50节点作为值输入20节点后20节点会作为控制节点输入给30节点,该节点为分支节点该节点通常会有true与false两个输出节点,判断是true还是false的依据就是分支节点的第一个输入节点,在此图中分支节点的第一个输入节点是25节点该节点负责用于比较23节点是否小于25节点,当结果为false时循环退出:
GraphBuilder阶段从GraphBuilderPhase::Run函数开始执行:
GraphBuilderPhase::Run函数会先根据数据信息来判断是否添加相应的标记,之后再去获取闭包(closure)对象,闭包对象用于获取要被优化的函数对象还有share_info等信息,闭包实际上是一个编程术语,在此处闭包对象就是为了能让子函数去获取父函数变量的对象,然后再创建一个频率对象,最后调用BuildGraphFromBytecode函数:
BuildGraphFromBytecode函数会根据字节码来创建节点图,该函数首先会根据传入的各项参数创建BytecodeGraphBuilder对象:
BytecodeGraphBuilder对象的构造函数实现并不复杂,但是要初始化的对象成员变量比较多,从中选择个人认为比较重要的几个对象进行说明,首先是JSHeapBroker对象该对象是JS堆的代理对象。Isolate是V8虚拟机的实例,它负责为Javascript源码创建执行环境,管理堆栈、编译、执行、context等所有组件。zone对象是用于内存管理,但是其管理的全部是临时内存而且他还是增量分配,一次销毁的,例如AST树还有逃逸分析阶段需要创建的VirtualObject对象等该内存在编译完成后被释放。jsgraph是在graph的基础上实现的外观,使用JS-specific的概念增强图形。native_context用于获取NativeContext对象,顾名思义就是编译目标的本机上下文。shared_info用于获取SharedFunctionInfo对象,该对象是优化函数的shared_info,在debug版本下通过%DebugPrint函数就可以查看到。bytecode_array是字节码数组用于获取字节码。feedback_cell用于获取FeedbackCell对象,该对象用于维护闭包与feedback_vector之间的链接。feedback_vector用于获取FeedbackVector对象,该对象是一个反馈槽该反馈槽,由于v8的优化是基于推测的,所以需要搜集信息来确认推测是否成立,如果不成立就需要解优化回退至字节码执行,所以该槽决定着优化代码是否需要解优化执行。
BytecodeGraphBuilder对象创建完成后通过调用BytecodeGraphBuilder对象的成员函数CreateGraph来创建图,该函数会先开辟一块临时内存区域,随后判断节点起源表是否为空,如果为空就创建并添加一个新的节点起源表,节点起源表用来存储字节码起源:
随后去设置图的基本结构并创建start节点:
在这其中OutputArityForFormalParameterCount函数用于计算start节点的输出节点个数,其计算逻辑大致为用函数及其接受器形式参数个数加上其他必要的输出节点个数:
然后CreateGraph函数会创建Environment对象,Environment中有一个values成员对象,该对象是一个Vector主要用来模拟保持字节码中使用的参数、寄存器、累加器:
当Environment构造函数开始执行时会先用参数初始化一些成员对象,初始化结束后就会开始初始化values_的布局,通常第一个参数都是receiver也就是this对象,创建并获取this parameter节点后将其存入values_中:
随后是寄存器的初始化处理,对于寄存器先通过values_的size来初始化对象的register_base成员变量,之后直接创建UndefinedConstant节点,然后依据所使用的寄存器个数插入values_中:
最后是累加器,几乎所有字节码指令都会用到累加器,在字节码操作累加器时并不需要显式的写出来,这样可以使字节码更短节省内存,在代码中先通过values_的size来初始化对象的accumulator_base成员变量,随后再将undefined constant节点插入values_:
然后还会去处理context parameter节点,用该节点去为对象成员变量context_赋值,不过要注意的是context parameter节点不会被存入values_中:
在初始化结束后values_应该是以下布局:
| index | content | node |
|---|---|---|
| 0 | receiver | parameter[0,debug name: %this] |
| 1 | arg0 | parameter[1] |
| 2 | reg0 | HeapConstant[undefined] |
| 3 | accumulator | HeapConstant[undefined] |
values_初始化结束后还需要判断寄存器是否不为空,对于不为空的要将values_中相应寄存器的UndefinedConstant节点替换为新的节点:
创建完Environment对象后,还会创建相应的FeedbackVector节点与NativeContext节点,之后通过VisitBytecodes函数依次访问字节码,并根据字节码创建对应的节点:
VisitBytecodes函数会先判断是否有已挂起需要恢复的生成器,如果有的话就创建SimConstant节点并将SimConstant节点与环境对象中的成员变量generator_state_绑定用于表示生成器的状态(正在执行):
随后去判断是否是OSR编译,OSR是一种在运行时替换正在运行的函数/方法的栈帧的技术,关于OSR编译我的理解是v8在执行优化代码的时候,不会一直等执行完编译优化再从头执行jit代码或是等优化完成后再从头重复执行已经执行过的代码,他会先去执行字节码,优化编译时也不会编译整个函数更不会再去编译已经在执行或者执行过的那部分,但是那部分的值和栈信息也要进行同步以便于编译优化过的机器码执行,所以就有了osr(On-Stack Replacement)机制,一般OSR编译都是编译循环部分开始的部分,也就是说当一个循环在执行时也会先去执行字节码,当执行多次后编译优化完成,通过osr将栈帧信息同步,随后在下一次执行时进入编译优化后的机器码继续执行循环:
当是osr循环时,会进入AdvanceToOsrEntryAndPeelLoops函数剥离OSR循环和包含它的任何外部循环,通过AdvanceToOsrEntryAndPeelLoops函数的注释可知,该函数会遍历OSR循环,然后再遍历它的父循环以此类推直到最外层的循环,最外层循环以外的部分不会为其生成节点:
随后调用FillWithOsrValues函数为每个环境创建对应的OsrValue节点:
然后再去创建JSStackCheck节点:
BuildOSREntryStackCheck函数会直接创建一个JsFunctionEntry类型的JSStackCheck节点,随后去准备JSStackCheck节点的FrameState输入/依赖节点:
再之后开始处理当前循环并依次访问字节码,根据字节码生成节点,随后剥离外一层循环从头开始执行相同的节点生成操作,直到最外层循环,最外层循环不在此处生成节点它将与osr循环所在的函数其他部分进行节点生成:
之后就会回到VisitBytecodes函数,刚才是对于osr编译的分析,而对于非osr编译则会直接进入BuildFunctionEntryStackCheck()函数创建JsFunctionEntry类型的JSStackCheck节点并且创建其依赖的FrameState节点:
然后会遍历字节码迭代器,并通过VisitSingleBytecode函数去依次处理字节码指令:
VisitSingleBytecode函数先获取光标当前所在偏移,光标用于标注当前所在字节码的位置,用光标位置减去起始地址再减去字节码前缀大小即可得到当前所处理的字节码偏移位置:
然后再用得到的字节码偏移去更新字节码指针:
之后会先去合并控制流,随后判断环境对象是否为空,如果不为空才会再去执行之后的步骤,否则将直接退出
然后会通过switch来进入每条的字节码指令相对应处理函数生成相应的节点并进行基础的优化,函数格式均为Visit(字节码名称):
以数组元素访问为例,假设有以下代码:
function opt(o){
let i = 0;
return i + o[3];
}
%OptimizeFunctionOnNextCall(opt);
opt([0,1,2,3]);
%SystemBreak();
该代码生成的字节码如下,其中o[3]对应字节码中的第2、4行,LdaSmi将3存入累加器中,随后通过GetKeyedProperty获取元素:
先是LdaZero,LdaZero负责将常数0存入累加器中,该字节码会触发VisitLdaZero函数,该函数逻辑比较简单,直接创建一个值为0的NumberConstant节点:
之后是Star0,Star0负责将累加器中的值放入寄存器r0中,该字节码会触发相应的VisitStarX函数,X代表寄存器编号,该函数将查找并获取累加器节点此处的累加器节点就是上一步中创建并绑定的ZeroConstant节点,然后调用BindRegister函数绑定寄存器与累加器节点,BindRegister函数中会调用PrepareFrameState函数,此函数中会判断获取到的累加器节点是否需要FrameState节点,如果需要就会创建FrameState节点作为累加器节点的输入节点:
随后是LdaSmi [3],LdaSmi [3]将常数3存入累加器,该字节码会触发VisitLdaSmi函数,该函数会先获取第一个操作数并创建该操作数相应的NumberConstant节点,随后将该操作数节点绑定到累加器中:
然后是GetKeyedProperty a0, [1],a0代表函数的第一个参数(ai,a代表参数i代表第几个)该参数作为object,[1]代表feedback插槽,而数组下标key/name则保存在累加器中,先查看该字节码的实现函数,函数中第576行从寄存器中得到object,第577行从累加其中得到key/name,第578行得到插槽,第579行得到反馈向量,第580行得到寄存器的当前执行上下文,随后583行调用Builtin函数KeyedLoadIC从inline cache中获取数据,第585行再将所得结果再存入累加器中:
然后再来查看BytecodeGraphBilder中对该字节码的处理流程,该字节码会触发VisitGetKeyedProperty函数进行处理,VisitGetKeyedProperty函数会先调用PrepareEagerCheckpoint函数准备Checkpoint节点,PrepareEagerCheckpoint函数先判断是否需要创建Checkpoint节点,如果需要就再调用environment()->Checkpoint函数为checkpoint节点创建一个前置的FrameState节点,然后再创建Checkpoint节点:
如果不需要创建,那就从当前效果依赖开始遍历所有效果依赖输入,直到找到一个Checkpoint节点,并将当前效果依赖输入替换为找到的Checkpoint节点:
调用完PrepareEagerCheckpoint函数后,VisitGetKeyedProperty函数会先通过累加器得到key,在当前代码中key为一个NumberConstant节点
之后再调用LookupRegister函数通过查找寄存器来获取object,在当前代码中object为Parameter节点对应字节码中的a0:
随后创建反馈源对象,索引为1
之后创建节点的opcode对象,此处创建了JSLoadProperty节点,再尝试对该节点进行初步的简化并返回简化结果,最后判断简化结果是否需要退出,如果需要就直接返回不去调用NewNode函数创建节点:
查看TryBuildSimplifiedLoadKeyed函数实现,TryBuildSimplifiedLoadKeyed函数会去调用ReduceLoadKeyedOperation函数,ReduceLoadKeyedOperation函数通过调用BuildDeoptIfFeedbackIsInsufficient函数来尝试构建解优化(Deoptimize)节点如果节点创建成功就会返回kind为Exit的简化结果:
BuildDeoptIfFeedbackIsInsufficient函数在以下两种情况会直接返回空值,第一个if检查是否没有初始化救援(用于确定是否需要解优化以及解优化的原因),第二个if会去检查反馈是否充足(用于搜集信息以确定是否需要解优化),如果救援没有初始化或者反馈不充足就直接返回空,判断反馈是否充足的代码逻辑在IsUninitialized函数中,主要是判断依据是inline cache的状态:
除去以上两种情况BuildDeoptIfFeedbackIsInsufficient函数将会创建一个新的Deopt节点并查找一个FrameState节点作为其输入节点最后返回,在此处就会直接创建一个Deoptimize节点:
直接结束后返回到VisitGetKeyedProperty函数,由于result kind为Exit所以在此处会直接退出:
假如在BuildDeoptIfFeedbackIsInsufficient函数中Deopt节点创建失败(返回nullptr)则result kind为NoChange,如果kind不为Exit则不会返回将继续往下执行,在之后的代码中则会创建JSLoadProperty节点或者从简化对象中获取节点(只有在简化结果的kind为SideEffectFree时才会去获取value,否则value为空)并于累加器绑定:
最后理论上会去执行VisitAdd(或VisitAddSmi)函数与VisitReturn函数,依次来生成字节码Add与Return的节点,但是由于在BytecodeGraphBuilder::VisitGetKeyedProperty函数中由于Deoptimize节点创建成功,所以简化结果kind为Exit,简化结果kind为Exit会直接导致在应用简化结果时将环境对象置为空
而环境对象为空在VisitSingleBytecode函数中则会直接返回不再执行后续步骤,自然也无法触发VisitAdd与VisitReturn函数
还有一种例外的情况那就是如果存在控制流合并的情况,如果确认存在控制流合并的情况则可能会影响到环境对象: