coconilu/Blog

V8引擎的特性

coconilu opened this issue · 0 comments

V8特性

为了让JS运行的更快,V8引擎衍生了很多特性。

字节码

JS代码最终都是需要编译成机器码才能被执行的。

早期的 V8 为了提升代码的执行速度,直接将 JavaScript 源代码编译成了没有优化的二进制的机器代码(基线编译器),如果某一段二进制代码执行频率过高,那么 V8 会将其标记为热点代码,热点代码会被优化编译器优化(优化编译器),优化后的机器代码执行效率更高。

但是,这会带来两个问题:

  1. 时间问题:编译时间过久,影响代码启动速度;
  2. 空间问题:缓存编译后的二进制代码占用更多的内存。

这两个问题都会影响 V8 在移动设备上的普及,所以 V8 团队重构了 V8 的架构,引入了字节码、解释器(Ignition)和新的优化编译器(TurboFan)。

从下图可以看出JS代码、字节码、机器码的所占空间大小:

image

虽然解释器执行字节码的速度没有机器码快,但是编译成字节码的速度比编译成机器码的速度快,所以整体执行效率不会大打折扣。然而,字节码可以节省大量的空间,且基于字节码的架构可以方便移植到不同的CPU架构平台。

即时编译

前面提到了字节码,当一段代码刚开始被执行的时候,只有字节码的形式存储在内存,当这段字节码(函数)被检测到多次执行的时候,会被转换成机器码,机器码是根据类型(隐藏类)来确定执行的,如果执行同一个函数多次,并且传入的参数和返回的数据类型不变的话,可以执行相应的机器码,如果不同则会执行优化回滚,删除机器码并使用字节码执行。

这也是当下这么多检查类型的工具的原因,比如flow、typescript,良好的编程规范可以带来更快的执行效率。

隐藏类

为了提升访问对象属性的效率,V8借鉴静态语言的优势,引入了隐藏类的概念。

静态语言可以直接通过偏移量查询来查询对象的属性值,这就是静态语言的执行效率高的其中一个原因。

V8 基于两个假设创建了隐藏类,也就是对象的"形状":

  1. 对象创建好了之后就不会添加新的属性;
  2. 对象创建好了之后也不会删除属性。

每个对象都有一个 map 属性,其值指向内存中的隐藏类,如果两个对象的"形状"是相同的,V8 则会复用同一个隐藏类。

"形状"有严格的要求,形状相同要求两个对象的属性名和属性值类型要相同,且属性的数量和创建顺序也要相同。给一个对象添加新的属性,删除新的属性,或者改变某个属性的数据类型都会改变这个对象的形状,那么势必也就会触发 V8 为改变形状后的对象重建新的隐藏类。

所以有如下的最佳编码实践:

  1. 尽量使用类(class)来创建对象,因为相同的类(class)创建的对象指向同一个隐藏类
  2. 如果一定要使用字面量初始化对象,确保对象的属性的顺序相同且完整
  3. 避免使用delete操作符

内联缓存

虽然对象访问可以通过隐藏类来加速效率,但是对于函数是第一公民的JS来说,V8可以做得更多。

内联缓存(Inline Cache,IC)就是为了加快函数执行效率而提出的概念。

对于每一个函数,V8都用一个反馈向量(FeedBack Vector)来存储与执行相关的信息,包括加载对象属性 (Load)、给对象属性赋值 (Store)、还有函数调用 (Call),每条向量也叫一个插槽,每个插槽中包括了插槽的索引 (slot index)、插槽的类型 (type)、插槽的状态 (state)、隐藏类 (map) 的地址、还有属性的偏移量。

比如如下代码:

function foo() {}
function loadX(o) {
  o.y = 4;
  foo();
  return o.x;
}
loadX({ x: 1, y: 4 });

loadX函数中的三行代码分别代表Store、Call、Load,Store和Load会存储隐藏类和偏移量,下一次执行loadX传入的参数的隐藏类不变的话,执行效率将会提高。

如果下一次执行loadX的入参的隐藏类不一样,那么就会触发多态、甚至超态,一个插槽存储多个隐藏类的信息。

如果一个插槽中只包含 1 个隐藏类,那么我们称这种状态为单态 (monomorphic);如果一个插槽中包含了 2~4 个隐藏类,那我们称这种状态为多态 (polymorphic);如果一个插槽中超过 4 个隐藏类,那我们称这种状态为超态 (magamorphic)。

对象存储

JS中存在大量的对象,每一个对象像一个字典,字符串作为键名,任意对象可以作为键值,可以通过键名读写键值。

在V8中,对象中的数字属性称为排序属性,在 V8 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。这是因为在 ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。

image

如上所示,如果执行索引操作,那么 V8 会先从 elements 属性中按照顺序读取所有的元素,然后再在 properties 属性中读取所有的元素,这样就完成一次索引操作。

那么问题来了,在查找元素时,多了一步操作,比如执行 bar.B这个语句来查找 B 的属性值,那么在 V8 会先查找出 properties 属性所指向的对象 properties,然后再在 properties 对象中查找 B 属性,这种方式在查找过程中增加了一步操作,因此会影响到元素的查找效率。

基于这个原因,V8 采取了一个权衡的策略以加快查找属性的效率,这个策略是将部分常规属性直接存储到对象本身,我们把这称为对象内属性 (in-object properties)。

对象内属性的数量是固定的,默认是 10 个,如果添加的属性超出了对象分配的空间,则它们将被保存在常规属性存储中。虽然属性存储多了一层间接层,但可以自由地扩容。

参考

极客时间——李兵的《图解 Google V8》
极客时间——李兵的《浏览器工作原理与实践》