Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V8引擎的特性 #166

Open
coconilu opened this issue Jun 10, 2020 · 0 comments
Open

V8引擎的特性 #166

coconilu opened this issue Jun 10, 2020 · 0 comments

Comments

@coconilu
Copy link
Owner

coconilu commented Jun 10, 2020

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》
极客时间——李兵的《浏览器工作原理与实践》

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant