JSConf EU 2018圆满结束, 谷歌V8的开发者Mathias Bynens以及Benedikt Meurer一起发表了《JavaScript Engines: The Good Parts™》演讲,本文将带领大家回顾一下演讲上所提到的重点。
演讲第一部分: JavaScript引擎
JavaScript引擎
JavaScript引擎解析源代码并将其转换成抽象语法树(AST)。基于AST,解释器产生字节码。此时,引擎正在运行JavaScript代码。为了加快运行速度,字节码连同分析数据一起发送到编译器。编译器根据已有的分析数据做出某些假设,然后生成优化后机器代码。
JavaScript引擎中的解释器/编译器
通过对比主流JavaScript引擎之间的一些实现差异来说明JavaScript引擎是如何运行你的代码。
解释器快速生成未优化的字节码,编译器会花费更长的时间,但最终产生高度优化的机器代码。
以上基本就是V8在Chrome和Node.js中的工作流程
V8的解释器负责生成和执行字节码。当它运行字节码时,它收集分析数据,这些数据是优化的依据。当函数运行时,生成的字节码和分析数据被传递给TurboFan编译器,基于分析数据生成高度优化的机器代码。
解释器可以快速生成字节码,但字节码通常执行效率不高。另一方面,编译器需要更长的时间,但最终会产生更高效的机器代码。快速获取代码以运行(解释器)或占用更多时间,但最终以最佳性能运行代码(编译器)之间存在权衡。
演讲第二部分:JavaScript的对象模型
ECMAScript规范基本上将所有对象定义为字典,并将字符串键映射到描述对象。
演讲第三部分:属性的访问优化
属性访问是JavaScript程序中最常见的操作。对JavaScript引擎来说,快速访问属性是至关重要的。
const object = {
foo: 'bar',
baz: 'qux',
};
// Here, we’re accessing the property `foo` on `object`:
doSomething(object.foo);
// ^^^^^^^^^^
Shape
在JavaScript程序中,具有相同属性键的对象是常见的。这样的对象具有相同的Shape。
const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };
// `object1` and `object2` have the same shape.`
在相同Shape的对象上访问相同的属性也是非常常见的:
`function logX(object) {
console.log(object.x);
// ^^^^^^^^
}
const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };
logX(object1);
logX(object2);
所以,JavaScript引擎可以基于对象的Shape优化属性的访问。
假设我们有一个属性为x和y的对象,它使用我们前面讨论过的字典数据结构:它包含作为字符串的键,并且他们指向各自属性的描述对象。
如果每个JS对象都存储描述对象,会造成大量的重复和不必要的内存开销。JavaScript引擎会将这些对象的Shape分开存储。
所有JavaScript引擎都使用Shape作为优化,但它们并不都称之为Shape:
- 学术论文称之为Hidden Classes
- V8称之为Maps
- Chakra称之为Types
- JavaScriptCore称之为Structures
- SpiderMonkey称之为Shapes 演讲中统一使用了Shape。
过渡链与过渡树
如果一个对象指向某个Shape,你给它添加一个新的属性,JavaScript引擎如何找到新的Shape。这类Shape在JavaScript引擎中形成所谓的“过渡链”。下面是一个例子:
我们甚至不需要为每个Shape存储完整的属性表。相反,每一个Shape仅需要知道它所引入的新属性。例如,在这种情况下,我们不必在最后一个Shape中存储关于“x”的信息,因为它可以在链中更早地找到。为了做到这一点,每一个Shape都和上一个Shape产生链接:
但是如果没有办法创建一个过渡链怎么办?例如,如果有两个空对象,并且向每个对象添加不同的属性呢?
const object1 = {};
object1.x = 5;
const object2 = {};
object2.y = 6;
在这种情况下,我们必须使用分支取代链,我们最终得到一个过渡树:
引擎对已经包含属性的对象应用了一些优化。要么从空对象开始添加“x”,要么有一个已经包含“x”的对象:
const object1 = {};
object1.x = 5;
const object2 = { x: 6 };
内联缓存(ICs)
ICs是使JavaScript快速运行的关键因素!JavaScript引擎使用ICs来记住在何处查找对象属性的信息,以减少查找次数。 这里有一个函数getX,它获取一个对象并从中加载属性“x”:
function getX(o) {
return o.x;
}
如果我们在JSC中运行这个函数,它会生成下面的字节码:
JSC还将内联缓存嵌入到get_by_id指令中,该指令由两个未初始化的槽组成。
现在假设我们使用{x:“a”}参数来调用getX。如我们所知,这个对象指向有属性“x”的Shape,并且该Shape存储了属性“x”的偏移量和描述对象。当第一次执行该函数时,get_by_id指令查找属性“x”,并发现该值被存储在偏移量0。
嵌入到get_by_id指令中的IC记住了这个属性是从哪个Shape以及偏移量中找到的:
演讲第四部分:有效的存储数组
数组使用数组索引来存储属性。这些属性的值称为数组元素。为每个数组元素存储描述对象是不明智的。数组索引属性默认为可写、可枚举和可配置,JavaScript引擎将数组元素与其他属性分开存储。
看一下这个数组:
const array = [
'#jsconfeu',
];
引擎存储的数组长度为1,并指向包含length的Shape,偏移值为0。
如果更改数组元素的描述对象,会怎么样?
// Please don’t ever do this!
const array = Object.defineProperty(
[],
'0',
{
value: 'Oh noes!!1',
writable: false,
enumerable: false,
configurable: false,
}
);
上面的代码段定义了一个名为“0”的属性(恰好是一个数组索引),但它将属性设置为非默认值。
在这样的极端情况下,JavaScript引擎将整个元素后备存储区作为字典,映射描述对象到每个数组索引。
结语
本次演讲让我们明白JavaScript引擎是如何工作的,如何存储对象和数组,以及如何通过Shape和ICs优化了属性的访问,如何优化了数组的存储。基于这些知识,为我们确定了一些实用的可以帮助提高性能的编码技巧:
- 总是以同样的方式初始化对象,它们最终会有相同的Shape。
- 不要修改数组元素的描述对象,它们可以有效地存储。
注记
- 本文结构及代码来自 Mathias Bynens以及Benedikt Meurer 在 JSConf EU 2018 上所作的演讲 JavaScript Engines: The Good Parts™。录像地址:https://www.youtube.com/watch?v=5nmpokoRaZI&index=11&list=PL37ZVnwpeshG2YXJkun_lyNTtM-Qb3MKa
- 同时也可以阅读本次演讲的Blog:https://mathiasbynens.be/notes/shapes-ics
原文作者:想成为工匠的码农
本文来源: 掘金 如需转载请联系原作者