Source to Binary Jounrney of V8 javascript engine

What is V8?

v8是Google的Javascript引擎,它被采用作为Google Chrome/Node.js的Javascript引擎。

Execution flow

Parsing

Basic parsing

v8解析源代码并将其转换为AST抽象语法树

Split parsing phase

首先,直接解析所有的代码并不是很好,如果解析的代码没有被执行,这是没有意义的。
为了延迟解析,将parse分为两个阶段。

PreParsing

事先解析所有函数的布局。


v8::internal::PreParser Class一次解析函数的轮廓。
由此得到

Abstract Syntax Tree

V8自己实现的解析器,不使用编译器编译器,如yacc或lex。使用递归下降语法分析进行解析。

Subsclass constructor return

修改继承类中的构造函数
在派生类的构造函数中返回表达式
转换为三元运算符
如果表达式的结果未定义,则返回该值。

for(let/const/var in/of e)

为了在for-in/of的初始化中使用const/let
通过将其封闭在一个块中,来声明一个变量

Spread operator

在JavaScript的语法中,有一个名为Spread运算符的语法。
var x = [1,2,3];
var y = [… x];
V8会将此语法重写为完全不同的语法,如Altus的Transpiler。


通过这种方式,我们将其重写为等效的do和for of语法

Ecmascript? – Binary AST

正如我们所看到的,AST的大小非常大,所以我们建议压缩它。

Ignition

Bytecode Interpreter

v8在执行之前将生成的AST转换成1到4字节的bytecode

How does it work?

Ignition是一种基于寄存器的字节码解释器,它实际上将值分配给CPU的寄存器,并执行它们。
在Ignition中,预先生成一个名为BytecodeHandler的字节码处理函数,从字节码中获得一个数组索引。
将生成的汇编代码分配给该索引,一个接一个地调用Bytecode数组,调用相应索引的汇编程序并执行代码。

Pseudo javascript code

用Javascript来模拟这个结构,看起来像这样。

How to create bytecode?

V8准备一个称为v8::internal::AstVisitor的基类,简称AstVisitor,从AST生成bytecode。
AstVisitor是一个使用Vistor模式的类。
在深度优先搜索AST时调用相应的回调函数。

BytecodeArray

生成的bytecode存储在BytecodeArray中。
BytecodeArray在函数基础上存在。

InterpreterEntryTrampoline

最终生成的字节码是从被称为InterpreterEntryTrampoline的Builtin代码执行的。
InterpreterEntryTrampoline被编译成Assembly,并且被当成普通的C函数调用。

Ignition Handler

前面伪代码中显示的BytecodeHandlers是V8
它被称为Ignition Handler
DSL中描述的Ignition Handler被称为CodeStubAssembler

Code Generator

v8中有几个汇编生成点,如下

  • CodeStub
  • Builtins
  • Runtime
  • BytecodeHandler
    我明白是从Bytecode运行汇编程序,但是相应的汇编程序如何从Bytecode生成?什么是BytecodeHandler?

CodeStubAssember

What is CodeStubAssmber?

CodeStubAssembler(CSA)将抽象代码生成为v8内部的graph generation DSL。
CodeGenerator只需组装预定的执行节点,即可为每个架构生成代码,因此您不必每次都编写汇编代码。

Graph based DSL


这个代码可以创建一个Graph,在执行时去使用它生成汇编。
Graph是使用DSL语言实现的,与c++代码的实际流程不同。

由于使用了CodeStubAssembler,即使您不熟悉实际体系结构的汇编程序,也可以轻松的添加新代码。
而且可读性也非常高。

Assembler

让我们来看看为每个架构实际输出代码的x64的jmp助记符

Where is actually outputting the assembler in Code Generation

名为MacroAssembler的类扮演着其角色。
虽然MacroAssembler的接口与体系结构无关,但在其内部调用的Assembler类会输出特定于每个体系结构的代码。
在V8中,MacroAssembler经常以属性名称masm频繁出现

最后,GraphResolver遍历Graph,通过MacroAssembler调用Assembler并输出汇编程序。

但是,MacroAssembler是进行最终的代码输出,底层抽象opcode嵌入在Graph中。
通过下面的层次结构,生成一个architecture-specific Graph。

  • CodeStubAssembler
  • CodeAssembler
  • RawMachineAssembler
  • MachineOperatorBuilder
    你越往下走,就越具体。

Where to use

Builtins使用Assembler class为不同架构生成Stub
一些部分使用CSA(*.gen.cc)
几乎所有的ignition handlers在CSA中有描述。

Builtins & Runtime

Builtins

Builtins是在v8启动时被编译好的asm code fragment
Call Builtin就像call一个函数
也被称为Stub
没有进行runtime优化

Runtime

Runtime是可以从Builtins和其他汇编代码中调用的c++代码
连接javascript和c++
也没有runtime优化

Inline Caching

What is Inline Caching

缓存之前的访问去加速property访问速度

  • 第一次访问
  • 第二次以后的访问

    Search Property


    为了从对象中找到property
    从HashMap或者FixedArray加载属性
    但每次都很慢

    Reduce Property Access

    在这个例子中,对具有相同Map的对象多次执行对y的访问。
    由于obj已经知道Map(x,y)…
    当然我们也知道内存布局,所以通过直接指定偏移量来访问会更快。

    Cache

    所以存储特定map的访问
    当访问你一个property的时候,Map object被记录
    这样做之后,第二次和随后的property访问被加速

    Cache Miss

    但是当Map更改的时候,自然会发生Cache miss,因此需要重新加载该属性并在此记住它。

    Load IC-Miss and StoreIC_Miss被调用,或者通过C++ runtime得到对象的属性。

Cahce State

Cahce State状态转变如下:
PreMonomorphic
Monomorphic
Polymorphic
Megamorphic

Pre Monomorphic

Uninitialized state->搜寻所用的隐藏类,以及获取位移。(参考上图第一次访问)

Monomorphic

Monomorphic是Receiver的类型不变时的IC,即这是在只访问single Map的理想情况
在这种情况下,一个缓存就足够了,所以它将是最快的情况。

Polymorphic

Polymorphic是在两种或更多类型的Receiver类型存在时被设置。
由于Polymorphic是循环搜索缓存的Map,找到应该使用的那个,所以它比Monomorphic慢,但比没有IC快得多。
(Map存储在FixedArray中,从多个Map搜索并执行属性访问的缓存)

Megamorphic

由于Miss太多,停止进行Map记录的状态。
通过从Stub调用GetProperty,来从哈希表中搜索,是获取properties最慢的状态。

Access inherited properties


summary

Optimization

Hot or Small

对满足以下条件的代码优化
(function字节码长度/1200)+ function被调用2次
函数很小(字节码长度小于90)

Optimization Budget

Optimization Budget(优化预算)在字节码执行期间被分配给每个函数,当其值低于0时,成为候选优化代码。

For loop

优化For循环
在循环中,输出称为JumpLoop的字节码
通过这个JumpLoop,返回终点的地址的值的偏移量被加权
从之前的Budget(预算)中扣除一个值,当它变成0的时候,对loop的优化将发生。


OSR - OnStackReplacement

在代码被编译和从bytecode到machine language之后,jump终点被改变,循环代码被切换到优化编译好的机器码。

For function

在函数调用的情况下,计算被Ignition生成的Return Bytecode在BytecodeHandler中的调用次数,如果超过一个阈值,就执行中断,并编译bytecode,替换原来的函数体。
(如果是函数,会生成一个Return bytecode,在此处中断并进行budget检查)


Concurrent Compilation

并行编译在对函数进行优化时是异步完成的,因此它不一定会针对后续函数调用进行优化


Budget for function

即使loop被分割,整个预算也会被计算出来,即使是Return也是如此,所以优化没有问题。

TurboFan

What is TurboFan?

TurboFan是V8的编译优化组件
在V8中,当bytecode的优化编译发生,它生成一个IR
TurboFan进行Graph generation和优化

IR

抽象执行块
Control Flow Graph

Optimization

TurboFan优化graph

  • inline
    内联函数调用
  • trimming
    未到达节点删除
  • type
    类型推断
  • typed-lowering
    根据类型将表达式和指令替换为更简单的处理
  • loop-peeling
    取出循环内的处理。
  • loop-exit-elimination
    删除Loop Exit
  • load-elimination
    删除不必要的读取和检查
  • simplified-lowering
    用更具体的值来进行指令的简单转换
  • generic-lowering
    将JS前缀指令转换为更简单的调用和stub调用
  • dead-code-elimination
    删除无法访问的代码

Code generation

最终,Class InstructionSelector分配寄存器
根据Graph,CodeGenerator生成机器码
将汇编代码写入PC(ProgramCounter)

Deoptimization

What is Deoptimization?

Deoptimization在有意料之外的值被传递到优化后的Assembly code时,对函数重新进行编译。
让我们看一个Deoptimization发生的例子

Wrong Map

在这个例子中,我们为第一个Map{(x)}输出一个优化的Assembly,
但是由于第二次调用的是{(x,y)}Map,因此它被强制重新编译
让我们看一下汇编

Bailout

检查map是否正确
当Deoptimization完成后,代码返回到字节码执行

Summary


以上是V8执行JS的过程
为了节省,省略了GC
(下载的pdf不能复制粘贴,大部分翻译是谷歌翻译相机拍下来然后识别翻译的,我尽量把我能理解的部分都纠正了)