JavaScript 引擎基本原理 - Shapes & Inline Caches

1,102 阅读3分钟

本文来源于对 mathiasbynens.be/notes/shape… 的学习总结

基础知识

浏览器引擎 pipeline

interpreter:可以快速生成未经过优化的字节码

optimizing compiler:可以生成高度优化的机器代码,但要花费更多时间

浏览器会在快速运行代码和花费更多时间生成更高效代码之间进行取舍,不同浏览器的具体实现不太一样,有些会添加多个不同 time/efficiency 的优化编译器,以增加复杂性为代价更细粒度地控制这些取舍。

Object Model

根据 ECMAScript 规范对 object 的定义如下:

数组的定义与对象类似:

优化属性读取

浏览器对对象属性的读取进行了优化,包括 shapes 和 inline caches.

Shapes

如果不同对象具有相同的属性键值,可以认为它们具有相同的形状(Shape)。

经过优化后的对象定义如下:

其中,a 和 b 是具有相同 shape 的 2 个对象,它们都具有属性 x y。shape 中包含所有属性的定义,其中没有 [[Value]],但是多了一个 offset -- 指定属性在对象内部值的偏移量;对象内部就只需存储属性值。

例如,读取 a.x,则先访问对应的 shape 实例,得知 x 的偏移量是 0,再取对象内部对应位置的值 5。

Transition chains and trees

如果对象属性增加或者删除,因此修改了 shape,如何找到新的 shape 呢?答案是 transition chain

上图中,o 先指向一个空的 shape,然后是具有 x 属性的 shape,最后指向具有 y 属性的 shape。每个 shape 只需要定义新引入的属性,然后链回之前的 shape。

下图是 transition tree 的示例,应该不难懂:

ShapeTable

如果对象的属性很多,根据前面的知识,将会在内存中创建很多个 shape,并且属性的读取也会变慢,时间复杂度为 O(n)。为了加快搜索速度,浏览器引入了 ShapeTable,ShapeTable 是一个 dictionary,将属性键值映射到指定的 shape。

const point = {};
point.x = 4;
point.y = 5;
point.z = 6;

似乎没有比优化前的对象定义好到哪去?这么做的原因是可以引入另一项优化 Inline Caches。

Inline Caches (ICs)

ICs 是让 JavaScript 快速运行的关键因素,引擎使用 ICs 存储对象属性的位置信息,以此来减少昂贵的查找次数。

下面的 function 在 JSC 中运行会生成如下字节码:

The generated bytecode is `get_by_id loc0, arg1, x`.第一条指令 get_by_id 从第一个参数 arg1 中加载 x,并存储到 loc0;

第二条指令返回在 loc0 存储的内容。

JSC 在 get_by_id 指令中嵌入了 IC,由 2 个未初始化的插槽组成 (上图中红色区域)。

现在调用一下 getX,传入参数 {x: 'a'}。该函数第一次执行时,get_by_id 指令会查找属性 x,发现 x 存储在偏移量 0 处,此时 IC 会记住找到的 shape 和 属性的偏移量。

对于后续的运行,IC 只需要比较 shape,如果与之前的相同,则从记住的偏移量处加载值即可,根本不需要接触属性信息,这比每次都要查找属性快得多。

由此可见,尽可能用相同的方式初始化对象,可以作为一条提升性能的 JavaScript 编码技巧。