Skip to content

Latest commit

 

History

History
212 lines (154 loc) · 9.14 KB

shapes-ics.md

File metadata and controls

212 lines (154 loc) · 9.14 KB

JavaScript engine fundamentals: Shapes and Inline Caches

JavaScript的对象模型(Object Modal)

根据 ECMAScript 规范的定义,JavaScript中的对象模型为一个字典结构,通过字符串的属性名引用Property Attribute对象,如下图所示: object-modal

我们可以通过 Object.getOwnPropertyDescriptor 这个api访问到对应的Property Attribute对象

数组其实是一类特殊的对象,只不过数组会对索引进行特殊的处理。 array-modal

注意:数组的索引有一个最大限额,为2**32 - 1个,也就是说,数组的索引范围为0 ~ 2**32-2,如果超过这个范围,则多出来的索引退化为普通对象的存储模式。

array-model

对象模型在内存中的存储

1. shape

首先,我们做出如下假设:

  1. JavaScript程序中会有很多的对象拥有相同的属性,我们称这些对象有相同的形状(shape)
  2. 访问具有相同形状的对象的同一个属性也很普遍

比如,

function logX(object) {
    console.log(object.x);
}

const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };

logX(object1);
logX(object2);

首先,作为字典,我们会如何存储JavaScript对象呢?

(此处应该有配图)

如上图所示,我们把property nameproperty attribute都存储在JSObject

这样存储的优点就是简单,缺点就是太浪费内存,因为相同shape的对象有相同的property nameproperty attribute,而我们却在每个对象上都存了一份,导致冗余。

因此,一个更节省内存的存储方式也就显而易见了,我们提取出一个shape对象,用来存储property name,并且这些property name指向的不再是property attribute, 而是类似的property information,唯一的区别就是[[value]]offset替代,我们的[[value]]实际存储在JSObject中,把偏移保存在property information中。 如下图所示: shape-1

这样存储之后,具有相同shapeJSObject会指向同一个shape对象,这样,相同的property nameproperty attribute只会保存一份。 如下图所示: shape-2 可以看出来,这种存储方式极大节省了内存。

2. transition chains and trees

假设现在有一个JSObject对象,它有一个对应的Shape对象,我们给这个JSObject动态添加一个属性,那么这个JSObject对象的Shape对象会 发生什么呢?

这里不卖关子了,当我们动态添加属性之后,对应的shape对象会过渡到一个新的shape对象,如下图所示: shape-chain-1

为了进一步节省内存,每一个shape只包含它所引入的property,如下图所示: shape-chain-2 这样,通过链表的方式把所有的shape链接起来

不同的书写方式,会导致不同的shape chain or shape tree,比如:

const object1 = {};
object1.x = 5;
const object2 = {};
object2.y = 6;

对应的shape如下: shape-tree

const object1 = {};
object1.x = 5;
const object2 = { x: 6 };

对应的shape如下: empty-shape-bypass

下面,我们实战一下

假设我们有如下代码:

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

其对应的shape chain如下: shape-table-1

假设我们现在要访问x属性,比如程序中写了x = point.x;JavaScript引擎需要从shape chain的最底部开始,顺着链表往上查找。 直到找到引入x属性的shape,然后获取其offset

如果我们频繁的这样查找,效率其实是很低的,尤其当我们的JSObject对象中的属性变多之后,这个查找速度是O(n)n是我们JSObject对象中的属性个数。

为了加快这个查找速度,JavaScript引擎引入了ShapeTable这个数据结构。这个ShapeTable是一个字典,用来在property name和引入该property nameshape对象之间建立映射。 如下图所示: shape-table-2 (这里其实是说,map的访问速度比linked list快)

这里我们又回到了字典这种数据结构了。我们在添加shape之前本就是字典这种数据结构,为何我们还要绕一大圈呢?

其实,我们主要是为了解决2个问题:

  1. 减少冗余数据的存储
  2. 加快对象属性的访问速度

我们引入shape之后,的确是减少了冗余的property nameproperty attribute的存储,具有相同shapeJSObject只会存一份数据, 但是对于对象属性的访问速度,由于我们又回到了原点(字典数据结构),因此并没有什么提升。

但是,我们基于shape的概念,出现了另一个技术Inline Caches,可以极大提升对象属性的访问速度。

Inline Caches (ICs)

假设我们有这样一段代码:

// 用来加载对象o的属性x
function getX(o) {
  return o.x;
}

如果用JSC运行这个函数的话,生成的字节码如下: ic-1 第一条指令get_by_id从第一个参数(arg1)中加载属性x,并存储在loc0中。 第二条指令返回这个loc0

如上图所示,JSC嵌入了一个ICget_by_id这条指令中,这个IC由2个未初始化的插槽(slot)组成。

现在,假设我们调用这个函数的参数对象是{ x: 'a' },如下图所示: ic-2

我们前面已经分析过了,这个对象有一个带x属性的shape对象,shape对象内部存储了x的偏移。 当你第一次执行这个函数的时候,get_by_id指令会去查找属性x(沿着shape linked list或者ShapeTable查找),最终找到这个属性的偏移是0。

这时候,内嵌到get_by_id指令里面的这个IC就会缓存这个结果,第一个插槽指向参数对应的shape对象,第二个插槽保存对应属性的偏移值。 如下图所示: ic-3

该函数后续的调用,IC只需要比较参数的shape是否发生了变化,如果没有变化,直接就可以根据缓存的偏移值去JSObject中加载值了。 如下图所示: ic-4 这样就可以避免掉昂贵的property information的搜索过程了。(图中的灰色)

高效存储数组

数组,我们前面已经说过了,是一种特殊的对象。特殊在哪里呢?数组的property name都是数字,我们称之为array indics,就是数组索引。 如果我们保存数组的每一个元素的property attribute的话,将是一笔不小的开销。

考虑下面这段代码:

const array = [
    '#jsconfeu',
];

引擎在数组对象中存储length属性的值(1),并指向它的shape对象,该shape对象和普通JSObject对象的shape并无区别。 如下图所示: array-shape

这和我们前面分析对象时差不多,但是,数组的元素值存储在哪呢?

如下图所示: array-elements

每一个数组都会有一个单独的elements backing store,这个地方用来存储数组的所有索引属性值。 还记得之前我们说过,数组的最大索引值为2**32-2,超过这个索引的元素值,将不会再存储在这个地方了。

JavaScript引擎不需要为这里面的每一个元素保存单独的property attribute数据,因为通常数组的所有元素都是可写、可枚举、可配置的。 也就是说,通常我们只需要为整个数组保存一个property attribute就可以了。

凡事都有例外,如果你非要修改一个数组元素的property attribute,那又会发生什么呢?

// Please don't ever do this!
const array = Object.defineProperty(
    [],
    '0',
    {
        value: 'Oh noes!!',
        writable: false,
        enumerable: false,
        configurable: false,    
    }
);

上面这段代码给一个空数组定义了一个属性'0',这个'0'正好也是一个数组索引值,但是却给它设置了一个非默认的property attribute

在这种极端情况下,JavaScript引擎将会把整个elements backing store作为一个字典存储,字典的key是数组的索引值,value是这个元素 的property attribute对象。 如下图所示: array-dictionary-elements

即便数组中只有一个元素是这种非默认的property attribute,整个数组的backing store也都会回退到这种低速且低效的字典模式。

因此,一定一定要避免在数组索引上使用Object.defineProperty

总结

我们了解了JavaScript引擎是如何存储对象和数组的,以及ShapesICs是如何用来优化对象的常见操作的。 基于这些知识,我们可以识别出一些JavaScript编码的最佳实践,来帮助提升性能:

  1. 总是用同样的方式初始化对象,以便他们可以有相同的shape
  2. 不要在数组元素上搞property attribute,以便他们可以高效存储和操作。