Skip to content

V8 与 JIT

JIT

即时编译(英语:Just-in-time compilation),动态编译的一种形式,是一种提高程序运行效率的方法。

通常,程序有两种运行方式:静态编译与动态解释。静态编译的程序在执行前全部被翻译为机器码,而解释执行的则是一句一句边运行边翻译。

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

JIT运用的地方很多,如.NET、JVM、PyPy等。在多个JS引擎均使用JIT技术提升js执行速度。

解释与编译

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

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

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

什么是机器码
  1. 机器码(Machine Code)是计算机的底层指令集,由二进制数表示。它是由硬件(中央处理器、内存等)直接执行的。机器码是由编译器或汇编器将高级编程语言(如 C、C++、Java 等)编写的源代码翻译而来的。

  2. 机器码是一组二进制指令,每条指令都执行特定的操作,如加载数据、存储数据、算术运算、控制流等。机器码是特定于硬件架构的,因此不同类型的计算机(如 x86、ARM、MIPS 等)具有不同的机器码。

  3. 在机器码中,每条指令都由一个或多个字节表示,并且可以包含操作数和寻址模式。操作数可以是立即数、寄存器、内存地址等。寻址模式指定了如何获取操作数的值。

  4. 机器码的执行速度通常比高级编程语言更快,因为它是直接在硬件上执行的。然而,编写、调试和维护机器码的过程非常困难,因为它与高级编程语言相比缺乏抽象和可读性。

V8将解释与编译结合

  • 为了解决解释器的低效问题,V8 把编译器也引入进来,结合了解释器和编译器两者优点设计了即时编译(JIT)的双轮驱动的设计,形成混合模式,给 JavaScript 的执行速度带来了极大的提升。
  • 不同的浏览器的JIT实现方式略有区别,但基本思想一致。

    即在js引擎中加入一个叫做monitor的部分来监控代码执行,并记录下每部分代码执行的次数以及使用的类型。如果monitor发现同一段代码执行多次,就会将此段代码标记为warm,如果执行次数非常多,就将其标记为hotwarm标记的代码会被放到baseline compilerhot标记的代码会被放到optimizing compiler

warm :baseline compiler

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

  • js引擎会为每一行代码生成一个"上下文","上下文"中包含代码行号和变量类型信息,当monitor发现同一段代码被执行,且其中变量类型相同,则会输出它之前已经编译好版本。
  • 编译器还能做一系列的优化工作,优化工作会花费一定的时间从而阻塞代码执行,但是如果某段代码执行的频率确实很高,那么牺牲一定时间来做这些优化工作是值得的。

hot :optimizing compiler

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

  • 但是 optimizing compiler 是基于一定假设的,比如,它假设通过某一构造器实例化出来的对象具有完全一样的结构,即拥有的属性相同,且属性顺序相同。
  • optimizing compiler 使用monitor收集到的代码信息来进行判断代码是否符合上述假设,在一个循环语句中,如果某个对象在前面的迭代中满足上面的条件, optimizing compiler 就会认为,在后续的迭代中,此对象依然会满足条件。
  • 但是由于JS的动态性,我们不能完全保证此假设成立,尽管是在同一循环期间,对象的结构也会发生变化。所以经过 optimizing compiler 编译的编译版本在执行之前需要进行检查,如果假设错误,将会丢弃此编译版本,然后将代码交回给解释器或者baseline cimpiler,此种行为被称为(弃优化)
  • 可见, optimizing compiler 机制可能会造成一些性能问题,如果js引擎针对某段代码在优化和弃优化直接不断循环,将会导致代码执行速度比没有使用优化机制更慢。

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


示例

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

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

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

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

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

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


存在额外的开销:

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

JIT 编译器优缺点

优点

  1. 性能提升:JIT 编译器可以将 JavaScript 代码编译为机器代码,从而使其在执行时更快。这可以显著提高 Web 应用程序的性能,特别是那些包含大量 JavaScript 代码的应用程序。
  2. 代码优化:JIT 编译器可以分析运行时代码的行为和数据,并进行优化。例如,它可以检测到循环中的不变表达式并将其编译为单个指令,从而提高代码的执行速度。
  3. 内存管理:JIT 编译器可以实现更高效的内存管理。它可以检测并回收不再需要的内存,从而避免内存泄漏和提高应用程序的稳定性。

缺点

  1. 初始启动时间:JIT 编译器需要将源代码或字节码编译为机器代码,这需要一定的时间。在应用程序启动时,可能会出现明显的延迟。
  2. 内存占用:JIT 编译器需要额外的内存来存储编译后的机器代码。这可能对设备的内存有一定的要求。(其实就是空间换时间)
  3. 调试困难:由于 JIT 编译器将 JavaScript 代码编译为机器代码,调试 JavaScript 代码时可能需要将机器代码反汇编回原始的 JavaScript 代码,这可能更加困难。