这是这篇文章的翻译,帮助了解 JS 引擎基础部分内容。
https://mathiasbynens.be/notes/shapes-ics
这篇文章介绍了一些关于 JS 引擎的基本概念Shapes和 Inline Caches,这有助于你理解代码性能。
JavaScript 引擎管线
引擎将源码转化为 AST(抽象语法树),然后解释器(interpreter)就可以生成字节码。
为了提高执行效率,字节码会跟 概要数据(profiling data) 一起发送到优化编译器,优化编译器根据 概要数据 作出一些的假设,生成高度优化的机器码。
如果有些假设是不正确的,优化编译器会反优化并会推到解释器执行。
解释器、编译器管线
解释器能很快的生成为优化的字节码,优化编译器随后话费相对更多的时间生成高度优化的机器码。
在Chrome 和 Node.js 使用的 V8 引擎中,解释器叫 Lngition,生成字节码,收集概要数据。概要数据之后可以提高执行效率。
当 一个函数经常调用,TurboFan(V8中的优化编译器)字节码和概要数据就会会生成高度优化的机器码。
SpiderMonkey 是 Mozilla 的 Firefox 浏览器和 SpiderNode 的引擎,实现起来就有些不同。这里就略过了。
这是微软的引擎管线,也有些不同。
为什么这些引擎都比 V8 多几个优化编译器呢?这是因为虽然解释器能很快生成字节码,但执行效率不高,而优化编译器虽然生成得慢,但机器码效率很高,多几个不同程度的编译器可以更精细的进行优化,提高执行效率。
JavaScript 的对象模型
接着看一下引擎如何实现 JS 对象模型的,以及如何加速访问对应属性。
ESMAScript规范定义了所有对象都是字典结果,并定义了一些属性的参数。
- Writable能否被写
- Enumerable 能否被枚举,比如在 for-in 中显示
- Configurable 能否被删除
可以使用Object.getOwnPropertyDescriptor
API来查看属性
对于数组,可以理解为一种特殊的对象。数组对下表有特殊的处理,数组长度限制为 2 的 32 次方- 1,数组有长度属性。
当有新的元素加到数组,JS 会自动更新 length 属性的 value 值。
优化属性访问
这里先介绍 shapes的概念
const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };
object1和 object2 有一样的 shape,shape 可以翻译为结构、索引?担心翻译得不准确,还是不翻了。
JS 引擎可以优化对象的属性访问,看看是如何实现的。
先看看属性参数是如何存在内存里面的呢?我们要把他们存到对象里吗?如果我们预测到之后会有很多一样 shape 的对象,那我们重复存就会造成浪费。引擎会作出优化,把 shape 单独存储。
shape 存储了除了 value 外的值,还存多了一个 offset用来标记属性的位置。
所有的 JS 引擎都有用 shapes 来做优化,不过有不同的名字:
- 学术论文里叫 Hidden Classes
- V8里叫 Maps
- Chakra (Edge 的引擎,查克拉?)里叫 Types
- JavaScriptCore(WebKit,苹果的)里叫Structures
- SpiderMonkey(Firefox)叫 shapes
shape 的链和树
当给对象增加属性时,shape 如何变化呢?
const object = {};
object.x = 5;
object.y = 6;
这里属性的添加顺序会影响 shape,也就是{x:4,y:5}和{y:5, x:4}用两个不同的 shape。
不过实际并不是如上图一样存储 shape的,而是如下图,有一个链的关系。当访问o.x属性时,会沿着链表去找 x 。
除了会有链表关系,还可能有树的关系。比如这个例子:
const object1 = {};
object1.x = 5;
const object2 = {};
object2.y = 6;
shape 也不一定从空开始,比如这个例子
const object1 = {};
object1.x = 5;
const object2 = { x: 6 };
object2初始化时就有 x 属性,对应的 shape 也有直接有 x,而不是从空的 shape 开始构造链表,这样相对更高效。
那如果像这样的代码
const point = {};
point.x = 4;
point.y = 5;
point.z = 6;
就会有三个 shape?然后访问某个属性时,去获取这个属性的参数就得顺着这个链表去遍历?
那读取某个属性的参数的时间复杂度就去到 O(n)了,JS 引擎为了优化这个时间,加多了一个叫 ShapeTable 的数据结构,其实就是一个map,做了属性名到 shape 的映射。
Inline Caches(ICs)
shapes 除了可以节省内存空间,还有一个作用就是帮助实现 Inline Caches,这是 JS 快的原因。
举个例子
function getX(o) {
return o.x;
}
这段代码是从对象 o 里读取属性 x
用 JSC(javascript complie?) 生成字节码后是这样的
第一句意思是get_by_id 命令从第一个参数 arg1 里读取属性 x,并存储结果到 loc0。
第二句是返回 loc0.
JSC在 get_by_id 里有两个内联缓存,就是红色的那两个 N/A
当执行一次后,第一个缓存会写为 shape (的内存地址?),第二个会写入属性的偏移量 0。
当频繁调用 getX 方法时,如果发现 shape 没变,会直接读取到 offset 为0,实际中,如果 JS 引擎看到 shape 在内联缓存记录过就能直接用了,不需要再去查。
高效存储数组
考虑有如下数组
const array = [
'#jsconfeu',
];
一般来说 JS引擎不会存储数组元素的参数,因为他们一般都是 wirable 、 enumerable 、 configurable 的。
但开发者也是有可能会改的,比如
// Please don’t ever do this!
const array = Object.defineProperty(
[],
'0',
{
value: 'Oh noes!!1',
writable: false,
enumerable: false,
configurable: false,
}
);
那 JS 引擎就会用 map 来存储下表和属性参数
即使数组只有一个元素有非默认的参数,整个数组的存储也会用这种更低效率的方法,逐个元素去查。因为应该避免在数组下标使用Object.defineProperty
方法。
建议
这篇文章是2018年写的,这是当时他给出的高性能写法建议:
- 用同样的方法初始化你的对象,这样就不会有不同的 shapes
- 不要改变数组元素的参数,这样他们能高效存储和检索
从 Shapes 和Inline Caches 这两个点可以看出 JS引擎为了提高 JS执行效率做在数据结构、缓存上做了不少优化。
在日常的业务开发中,我们好像也只是用到了这些功能,比如数据库对频繁读的数据做了缓存加快查询速度,比如对象用到了一样的数据和方法就把他抽象为类。可能是一种遗憾,没有机会像 JS 引擎一样能深入的研究、优化。
但业务开发也能对复杂的业务系统保持持续迭代、重构,保持代码可读性、可扩展性,这可能从另一个纬度来看,也是一种优化吧。