初识 JavaScriptCore JIT

3,217 阅读5分钟

今天我们来了解一下JavaScriptCore中的JIT机制。

一、JIT基本概念

JITJust In Time)编译器:是指程序逻辑以代码(或字节码)形式下发到目标机(如客户端)上,在系统即将运行此逻辑的前一刻,目标机系统上的编译器才将这些代码编译成机器指令,然后再交给系统执行。因为它的编译发生成运行前一刻,刚刚能赶得上执行,所以叫做Just In Time编译器.

谈到JIT,经常有同学把它与解释器(Interpreter)混淆,下面首先看一下这两个概念的区别:

解释器(Interpreter)和JIT的区别

虚拟机执行一段程序,一般有两种方式:解释执行和先编译再执行。

  • 解释执行:虚拟机读取程序字节码,取出其中的“虚拟机指令”,由解释器逐条进行解释执行

    • “虚拟机指令”可以理解为一种DSLDomain Specific Language),它作为一种领域专用数据结构,包含了虚拟机运行程序所需的所有数据信息(如:操作符、操作数等)
  • 先编译再执行,根据编译的时机不同,又可以分为AOTJIT

    • AOTAhead of Time,即开发者先将程序编译成机器码,再将由机器码构成的二进制程序下发到客户端运行。

      • JavaScriptCore目前不支持AOT
      • 一个支持AOT的虚拟机的例子是Dart VM,它可以执行事先编译成机器码的Dart程序。
    • JITJust in Time,虚拟机读取程序字节码,在真正运行代码逻辑前,先将他们编译成“机器指令”序列,再执行这些机器指令

      • JIT编译后,待运行的方法就已经是机器指令了
      • 一般对于比较“热”的方法可以在运行时动态调整JIT的级别,根据调用现场情况开启相应的优化

二、JavaScriptCore中的解释器和JIT

JavaScriptCore中的解释器(LLInt)和JIT都可以执行JavaScript代码编译成的字节码(bytecode[1]。而其中JIT又根据优化级别的不同分为三种:Baseline JITDFG JITFTL JIT

下面具体讲一下这四种模式的主要特点。

JavaScriptCore执行代码的四种模式

1. LLInt解释器模式

  • LLInt是用跨平台的汇编语言(offlineasm)实现的

  • 逐条解释执行JSC虚机指令

  • JS代码的执行总是从LLInt模式开始

LLInt切换到Baseline JIT的条件(满足任意一条即可):

  • 方法中任意一个语句执行次数超过100
  • 方法被调用了超过6

虚拟机由解释器模式向JIT模式切换时,解释器会将当前字节码偏移传给JITJIT只编译此字节码偏移能够到达的代码分支

OSR(On Stack Replacement):是一种可以在程序运行时动态切换其内部方法具体实现的技术

  • 方法切换可以在任意一条语句结束后
  • 是虚拟机在解释器和各JIT模式间无缝切换的关键技术保障

2. Baseline JIT模式

  • 只是做了简单的“机器码化”,减小了解释器按指令dispatch的开销,代码(指令序列)本身并未做任何优化。
  • 切换到DFG JIT的条件:
    • 方法中任意语句执行次数超过1000
    • 方法被调用次数超过66

3. DFG JIT模式(Data Flow Graph

DFG JIT 主要流程:

  • DFG会把字节码转成DFG CPS形式

    • CPS(Continuation-Passing Style)
      • CPS表示这样一种形式[3]:一个函数f除了它自身的参数外,总是有一个额外的参数continuationcontinuation也是一个函数,当f完成了自己的返回值计算之后,不是返回,而是将此返回值作为continuation的参数,调用continuation。所以CPS形式的函数从形式上看它不会return,当它要return的时候会将所有的参数传递给continuation,让continuation继续去执行。
      • CPS的优点是让如下的信息显式化:过程返回(调用一个continuation),中间值(具有显式的名称),求值顺序,尾调用(采用相同的continuation调用一个过程)。
      • CPS有利于后续通过profiling来预测数据类型,这些“数据类型预测”能减少后续生成代码时需添加的类型检查逻辑。
  • DFG启用了多种常规的编译优化

    • 寄存器分配
    • 控制流图简化
    • 公共子表达式消除
    • 无用代码消除
    • 稀疏有条件的常量传播
      • 编译时计算常量数据,并将它传播到相关代码中,达到整体简化代码的目的
  • DFG JIT编译器将CPS形式的代码优化后编译成机器码

例:如下的 foo 方法:

function foo(a, b) { return a + b + 42; } 
  • 经过LLIntBaseline JIT的多次运行后,profiler收集到了很多ab的运行时类型信息,如果发现都是int,则生成的机器指令可以是:先判断是否是int,如果是则跳转到类型固化为int的机器指令代码,否则OSR exitBaseline JIT

4. FTL JIT模式

FTL JIT主要流程:(复用了DFG的大部分流程)

  • 之前是使用的LLVM后端,后来改成了B3,上图还是旧的框架图
  • 复用了DFG的大部分流程,将原DFG流程中的DFG后端替换为新的FTL流程:
    • CPSSSA
    • SSALLVM IR
    • LLVM IR的编译优化
    • IR转机器码
  • 使用了LLVM后端,引入了更多的编译优化(类似C程序的极致优化)

参考资料