今天我们来了解一下JavaScriptCore
中的JIT
机制。
一、JIT
基本概念
JIT
(Just In Time
)编译器:是指程序逻辑以代码(或字节码)形式下发到目标机(如客户端)上,在系统即将运行此逻辑的前一刻,目标机系统上的编译器才将这些代码编译成机器指令,然后再交给系统执行。因为它的编译发生成运行前一刻,刚刚能赶得上执行,所以叫做Just In Time
编译器.
谈到JIT
,经常有同学把它与解释器(Interpreter
)混淆,下面首先看一下这两个概念的区别:
解释器(Interpreter
)和JIT
的区别
虚拟机执行一段程序,一般有两种方式:解释执行和先编译再执行。
-
解释执行:虚拟机读取程序字节码,取出其中的“虚拟机指令”,由解释器逐条进行解释执行
- “虚拟机指令”可以理解为一种
DSL
(Domain Specific Language
),它作为一种领域专用数据结构,包含了虚拟机运行程序所需的所有数据信息(如:操作符、操作数等)
- “虚拟机指令”可以理解为一种
-
先编译再执行,根据编译的时机不同,又可以分为
AOT
和JIT
:-
AOT
:Ahead of Time
,即开发者先将程序编译成机器码,再将由机器码构成的二进制程序下发到客户端运行。JavaScriptCore
目前不支持AOT
- 一个支持
AOT
的虚拟机的例子是Dart VM
,它可以执行事先编译成机器码的Dart
程序。
-
JIT
:Just in Time
,虚拟机读取程序字节码,在真正运行代码逻辑前,先将他们编译成“机器指令”序列,再执行这些机器指令JIT
编译后,待运行的方法就已经是机器指令了- 一般对于比较“热”的方法可以在运行时动态调整
JIT
的级别,根据调用现场情况开启相应的优化
-
二、JavaScriptCore
中的解释器和JIT
JavaScriptCore
中的解释器(LLInt
)和JIT
都可以执行JavaScript
代码编译成的字节码(bytecode
)[1]。而其中JIT
又根据优化级别的不同分为三种:Baseline JIT
、DFG JIT
和FTL JIT
。
下面具体讲一下这四种模式的主要特点。
JavaScriptCore
执行代码的四种模式
1. LLInt
解释器模式
-
LLInt
是用跨平台的汇编语言(offlineasm
)实现的 -
逐条解释执行
JSC
虚机指令 -
JS
代码的执行总是从LLInt
模式开始
由
LLInt
切换到Baseline JIT
的条件(满足任意一条即可):
- 方法中任意一个语句执行次数超过
100
次- 方法被调用了超过
6
次
虚拟机由解释器模式向
JIT
模式切换时,解释器会将当前字节码偏移传给JIT
,JIT
只编译此字节码偏移能够到达的代码分支
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
除了它自身的参数外,总是有一个额外的参数continuation
。continuation
也是一个函数,当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; }
- 经过
LLInt
和Baseline JIT
的多次运行后,profiler
收集到了很多a
和b
的运行时类型信息,如果发现都是int
,则生成的机器指令可以是:先判断是否是int
,如果是则跳转到类型固化为int
的机器指令代码,否则OSR exit
回Baseline JIT
。
4. FTL JIT
模式
FTL JIT主要流程:(复用了DFG的大部分流程)
- 之前是使用的
LLVM
后端,后来改成了B3
,上图还是旧的框架图
- 复用了
DFG
的大部分流程,将原DFG
流程中的DFG后端
替换为新的FTL
流程:CPS
转SSA
SSA
转LLVM IR
LLVM IR
的编译优化IR
转机器码
- 使用了
LLVM
后端,引入了更多的编译优化(类似C
程序的极致优化)