Skip to content

Latest commit

 

History

History
97 lines (59 loc) · 7.79 KB

V8与JIT.md

File metadata and controls

97 lines (59 loc) · 7.79 KB

V8与JIT

在众多的JavaScript引擎中,V8脱引而出,其优良的性能得到广大开发者的喜爱。

Google将webkit中集成的js引擎替换为V8: http://my.csdn.net/uploads/201207/18/1342625901_1361.jpg

我们熟悉的Node.js就使用了V8引擎来解析执行js,https://benchmarksgame.alioth.debian.org/u64q/compare.php?lang=node可以通过此链接查看Node与其他语言的基准测试比较。可以看到在V8中执行的JS可以是吊打其他脚本语言,与Java相比也相差不大。

V8采取了一系列优化机制提升JS效率,包括JIT编译 (JIT Compile)、垃圾回收(Garbage Collection)、内嵌缓存(Inline Cache)、隐藏类等。我们将从JIT垃圾回收等方面去了解V8内核。参考V8项目WIKI:V8的设计理念

JIT

即时编译(英语:Just-in-time compilation),动态编译的一种形式,是一种提高程序运行效率的方法。通常,程序有两种运行方式:静态编译动态解释。静态编译的程序在执行前全部被翻译为机器码,而解释执行的则是一句一句边运行边翻译。

即时编译器则混合了这二者,一句一句编译源代码,但是会将翻译过的代码缓存起来以降低性能损耗。相对于静态编译代码,即时编译的代码可以处理延迟绑定并增强安全性。

JIT运用的地方很多,如.NETJVMPyPy等。在多个JS引擎均使用JIT技术提升js执行速度: /assets/01-02-perf_graph10-768x633.png

解释与编译

通常有两种方法将高级语言代码转换为机器代码,解释编译

解释器一行一行的翻译代码,因此我们不需要等待所有代码都编译完成便可开始执行,能够尽快看到执行效果,这非常适合Web的场景;但是解释执行的问题是,解释器可能针对同一行代码重复翻译多次,如循环语句。然后解释器在翻译过程中不能花太多时间在各种优化机制上面。

编译的方式则相反,其在执行前将完整的程序翻译成机器码,执行的时候就不用考虑翻译的事情,所以就不会出现同一段代码重复翻译的问题。然后编译方式可以在翻译的时候花更多的事情在优化上面。

结合两种方式

为了解释器的问题即可能对相同代码重复翻译多次以及优化问题,js引擎可以在适当的时候混入编译方式。

不同的浏览器的JIT实现方式略有区别,但基本思想一致。即在js引擎中加入一个叫做monitor的部分来监控代码执行,并记录下每部分代码执行的次数以及使用的类型。如果monitor发现同一段代码执行多次,就会将此段代码标记为warm,如果执行次数非常多,就将其标记为hotwarm标记的代码会被放到baseline compilerhot标记的代码会被放到optimizing compiler

/assets/02-06-jit09-768x560.png

baseline cimpiler

当某段代码被标记为warm,将不直接通过解释执行,而是通过baseline cimpilerbaseline compiler会存储这段代码的编译结果:

/assets/02-05-jit06-768x565.png

js引擎会为图示中函数的每一行代码生成一个"桩","桩"中包含代码行号和变量类型信息,当monitor发现同一段代码被执行,且其中变量类型相同,则会输出它之前已经编译好版本。

编译器还能做一系列的优化工作,优化工作会花费一定的时间从而阻塞代码执行,但是如果某段代码执行的频率确实很高,那么牺牲一定时间来做这些优化工作是值得的。

Optimizing compiler

如果某段代码被标记为hot,这段代码会被放到Optimizing compilerOptimizing compiler会对代码进行更多优化,产生一个执行更快的编译版本并进行存储:

/assets/02-06-jit09-768x560.png

但是Optimizing compiler是基于一定假设的,比如,它假设通过某一构造器实例化出来的对象具有完全一样的结构,即拥有的属性相同,且属性顺序相同。

Optimizing compiler使用monitor收集到的代码信息来进行判断代码是否符合上述假设,在一个循环语句中,如果某个对象在前面的迭代中满足上面的条件,Optimizing compiler就会认为,在后续的迭代中,此对象依然会满足条件。

但是由于JS的动态性,我们不能完全保证此假设成立,尽管是在同一循环期间,对象的结构也会发生变化。所以经过Optimizing compiler编译的编译版本在执行之前需要进行检查,如果假设错误,将会丢弃此编译版本,然后将代码交回给解释器或者baseline cimpiler,此种行为被称为(弃优化):

/assets/02-07-jit11-768x555.png

可见,Optimizing compiler机制可能会造成一些性能问题,如果js引擎针对某段代码在优化和弃优化直接不断循环,将会导致代码执行速度比没有使用优化机制更慢。

所以,浏览器在某段代码在优化和弃优化直接徘徊到一定次数后,将会放弃优化。

优化实例:类型特化

Optimizing compiler针对代码优化有许多手段,其中比较典型的一个例子就是类型特化(type specialization),比如下面一段代码:

function arraySum(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
}

上面的代码看似简单,但是由于js是动态类型语言,在执行上面代码时,需要做许多额外的工作,当上面的代码被标记为warm时,将被交给baseline compilerbaseline compiler会为上面的每一行代码创建"桩",比如上面的sum += arr[i],来表示整形相加合赋值操作。

但是,我们不能保证sumarr[i]一定是整形,因为有可能arr的某一项并不是整形,如果其中一项是字符串类型,那么上面的加法操作执行策略就会不一样,需要被编译成新的版本。JIT的策略是对每一段代码产生多个"桩",如果该段代码在执行期间变量类型不变,只会产生一个"桩",否则会有多个"桩"。这样,该段代码每次执行前都需要进行一个"决策"过程:

/assets/02-08-decision_tree01-768x394.png

这样引擎将会花大量时间在询问相同问题(决策)过程中:

/assets/02-09-jit_loop02-768x496.png

Optimizing compiler的一个优化策略就是简化这样一个决策过程,对于其中一些变量的类型检查提前到循环之前:

/assets/02-10-jit_loop02-768x488.png

有些JIT实现会在这方面做更进一步优化,比如firefox定义了一种只含有整数的特别数组,如果arr满足条件,上面图中每次执行前关于arr[i]的类型检查也可省去。

存在的问题

上面的优化策略也会带来一些新的问题,其中之一就是会带来额外的开销:

  • 优化和弃优化的开销
  • monitor用于记录以及弃优化发生时恢复先前信息的开销
  • 存储baseline和优化版本的代码需要耗费一定内存

针对上面的问题,还可以通过进一步优化来解决,最近一段时间炒得火热的WebAssembly便是一种手段。我们会在下一章节进一步探讨。

此篇文章主要内容翻译自文章: https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/