Just-In-Time Compilers 以及v8引擎编译js过程

143 阅读4分钟

编程语言可以分成两类:一类是解释型语言,一类是编译型语言。

解释型语言

  • 优点:直接执行,在运行过程中进行编译,不需要经过单独编译的步骤。
  • 缺点:当某一段代码被重复执行时,每次都需要重复编译相同的代码

编译型语言

  • 优点:重复的代码只会编译一次,而且因为编译的时间相对比较充足,可以有更长的时间对代码进行编辑,所以编译后的代码运行的会更快。这里的编辑叫做优化。

JIT:两全其美的方式

  • 不同的浏览器会有不同的实现方式,但是基本的思路是一样的。浏览器一般都会给JavaScript引擎添加一个新的部分,监视器monitor。监视器用于监视代码的执行次数,并将多次执行的代码,标记为热点代码

这里以chrome浏览器的v8引擎解析js代码为例进行说明。

  1. js源代码会先在解析器(parser) 进行 词法分析,生成一个一个token,然后进行语法分析,生成抽象语法树(Abstract-Syntax-Tree)
  2. Ignition(解释器) 将AST转换成bytecode字节码 ,生成字节码后AST就会被清除掉,释放内存空间。生成的字节码直接可以被解释器执行。同时Ignition还会在执行的过程中收集信息(比如说类型信息),供TurboFan使用。
  3. TurboFan是一个编译器可以将字节码编译成CPU直接执行的机器码

v8引擎在处理js过程中的一些优化策略

  • 如果函数只是声明没有调用,则该函数不会被解析生成AST,也就不会生成字节码
  • 如果函数只是被调用一次,则Ignition生成字节码后就直接被解释执行了,TurboFan不会进行优化编译TurboFan优化编译是需要Ignition提供一些信息的(例如函数参数类型,返回值类型等等),这就要求函数被执行的次数要大于1次
  • 如果代码或者函数被多次执行,那么这些代码有可能被标记为热点代码,当多次执行这些热点代码过程中Ignition会收集这些信息,当这些代码的类型都是一样时,那么会假定(assumptions)这段代码的类型就是收集到的类型TurboFan会将字节码编译成优化后的机器代码,以提高代码的执行性能,之后执行该代码时,直接运行优化后的机器代码
  • 由于JavaScript 动态类型语言,也就是说JavaScript的类型是随时都有可能变化的。Ignition收集到的信息是有可能是错的,当执行过程中,发现之前收集到的类型是错误的时候,优化后的机器码会被逆向还原成字节码,这个过程叫做deoptimization去优化。字节码会转为汇编代码,然后转为字节码,然后运行。
  • 通常情况下优化后的机器代码会运行的更快,但如果一直进行优化和去优化的过程,某些情况下会导致性能低于直接执行字节码。通常情况下,浏览器都会限制优化和去优化,当一直处于这个阶段时,会打破它们。

通过代码进行说明deoptimization过程

// 数组的0-99项都是number类型,第100项是string类型
const arr = [0, ..., 99, '100'] // 
let sum = 0
for (let i = 0; i < arr.length; i++) {
   /**
   sum += arr[i]  这段代码会被执行多次,会被标记为热点代码
   在前100次循环中,Ignition收集到的类型是:
   变量sum为number类型,arr[i]为number类型
   那么TurboFan会将这段代码的生成的字节码编译成优化的机器代码
   当第101次执行该代码时,会发现arr[i]的类型是string类型,也就是说之前优化的机器代码不能使用了
   那么这个时候,就会进行deoptimization,将优化好的机器码转为字节码。
   所以保证类型的不变性,可以使我们的代码执行速度更快。
   */
    sum += arr[i]  
}