添加object classid=clsid 需要修改吗_一个由不可扩展对象(Object.preventExtensions)引发的性能问题...

在协作开发中,经常会遇到一个问题,自己定义的对象可能在未知的情况下,在别的地方被他人拓展或者删除了一些属性,导致出现不可控的问题。虽然可以依靠团队内的约定和开发规则来进行约束,但是还是存在风险。所以如果我们希望一个对象不被随意重写,就可以用到Object.preventExtensions ,Object.seal ,Object.freeze这些方法了。

简单介绍一下这几个方法

Object.preventExtension:让一个对象变的不可扩展无法添加新的属性,而且不可逆

Object.seal:等于在阻止添加新属性的基础上,让所有现有属性标记为不可配置,类似把所有属性的configurable都设为了false

Object.freeze:让一个对象被冻结,无法添加、删除已有属性,也不能修改该对象已有属性的可枚举性、可配置性、可写性,以及也不能修改已有属性的值。

一个例子

class FiberNode {   
  constructor() {     
     this.actualStartTime = 0;     
     Object.preventExtensions(this);   
  } 
} 
const node1 = new FiberNode(); 
const node2 = new FiberNode();

像上面这样一段代码,可以让FiberNode的实例对象不可被扩展。但事实上在某些情况下Chrome浏览器环境中,这样可能会引发严重的性能问题。

react的一个issue就是关于这个问题的(https://github.com/facebook/react/issues/14365)

我们知道react会创建很多FiberNode的实例,而actualStartTime会存储 performance.now()返回的一个精确到毫秒的 DOMHighResTimeStamp,例如424432.32132。这看上去似乎没有什么问题,那么到底是为什么会引发性能问题呢?让我们来一步步分析一下。

Hidden Classes 隐藏类

我们简化一下node1和node2,大概可以得到

const node1 = { actualStartTime: 0 };
const node2 = { actualStartTime: 0 };

像这样有相同属性,且属性的顺序,且属性的configurable、enumerable、writable这些都一致(值可以不同)的情况下,在v8里就称他们有相同的形状(Shape)

为什么要有Shape呢?因为很大部分对象的属性值这些configurable、enumerable、writable的信息都是相同的,然后也会出现像上面node1和node2这样属性名也相同的对象,如果每个对象都各自存储一份,就很浪费内存了,所以会把Shape分开存储。多个有相同Shape的对象就可以复用一个了。

ccd09753c246ce54147501c601e270c0.png
图源见参考资料

但由于js中对象可以动态的添加属性,那在添加了属性之后Shape又会产生什么变化呢?

这就涉及到了Transition 链。

Transition 链

当一个对象在后面又添加了属性时,就会如下图,继续创建一个Shape,包含引入的新属性。

之前的Shape会指向这个新的Shape,如果继续添加新属性则重复这个操作,形成一个类似双向链表的结构,称作Transition 链

而对象会指向那个最新的Shape。从末端遍历回起始,则可以得到整个对象的属性信息或者之前的某个属性的信息(当然这样如果想获取Transition 链中接近起始位置的属性效率不高,v8会通过ShapeTable等方式进行优化,我们这里暂时不细说)。

076fd37147443f4dc55d737464e5ce3b.png
图源见参考资料

而如果开始有多个对象指向同一个Shape,其中一个对象添加了属性则如下图:

03e71bb54f8214c4ab2013c46882eb0c.png
在之前图的基础上自己改的有点奇怪请不要介意 ‍♀️

当原本指向同一个Shape的对象,分别添加了不同的属性则不再是类似链表结构了,而是形成了一个类似树的结构。

c1422997cf4ec6a7f9b431e48a58d256.png
在之前图的基础上自己改的有点奇怪请不要介意 ‍♀️

我们再回到最初提到的Object.preventExtensions,在对一个对象进行了Object.preventExtensions之后,Transition 链又会变成怎么样呢?

c3f87cafbef5a2acfc6e3990f0079600.png
图源见参考资料

如图上所示,preventExtensions之后生成了一个新的Shape指向之前的Shape,但这个新的Shape不会关联新的属性信息,只是作为一个标记存在。

结合之前的FiberNode的代码来看,你可能会觉得完全没没问题啊,大家都以为是会变成如下图一样。

98480c837c5657d248611a337b190d96.png
在之前图的基础上自己改的有点奇怪请不要介意 ‍♀️

但事实上,如果b.x = 4的话,就是如图这样了,但b.x却是等于了一个小数。这有什么问题呢,我们继续看 。


number类型

我们都知道无论是9还是9.9还是-9在js中都是number类型,而在ECMAScript 标准中也规定了number是作为64-bit(双精度)浮点值来表示。但事实上js引擎为了性能的优化,在内部会用其他的方式来表示。

SMI 与 HeapNumber

例如31位有符号整数范围内的小整数,在v8引擎里会存储为SMI(Small Integer).而这个范围以外的number就会以堆对象(HeapObject)或者称为HeapNumber的方式来存储。v8对SMI有进行特殊的优化。

在v8中会将js的值(无论是对象、数字还是字符串)在v8堆上进行分配,然后就可以得到指向这个对象的一个指针。为了进行优化,v8使用了一项指针标记(pointer tagging)的技术,通过这个方式,存储一个smi就不需要额外的分配空间了,而值被存储到了指针本身。这样节约了内存空间,也提升了访问速度。

但仅仅是SMIHeapNumber的性能差别,一般还不至于引发我们上面提到的问题。别着急我们继续往下看。

MutableHeapNumber

let obj = {
    x: 2,
    y: 2.2
}

我们来看这么一个对象,从上文可以了解到,因为2是31位有符号整数范围内的小整数,所以可以以SMI的形式来存储。而2.2不属于这个范围,所以以HeapNumber的形式来存储,所以会有一个单独的内存实体来存储数值2.2,而obj.y指向这个实体。

当我们继续进行obj.x += 1的操作,因为加一后等于3,也属于smi范围,所以可以原地更新。但是y就不一样了,如果是obj.y += 1,又会重新生成一个新的HeapNumber,然后让obj.y指向这个新实体。

然鹅试想一下,如果不停的给y加一加一,按照上面说的方式需要生成多少个新的HeapNumber 即使有垃圾回收,也是一个巨大的浪费。

所以就出现了MutableHeapNumber,类似HeapNumber会分配空间存储它的值,但是允许直接更改它的值,而不需要每次都创建一个新的实体。

不过这样又引发了一个问题,当像这样

let y = obj.y;
obj.y += 1;

我们平时直观的感觉,好像理所应当因为 obj.y = 2.2,值是一个number又不是object类型,y是保持obj.y之前的值(2.2),不受后面obj.y的改变的影响。

但在v8引擎内部,如果obj.y采用了MutableHeapNumber,那在赋值给y的时候事实上是将MutableHeapNumber的引用给了y,那后续再发生修改,y再去取值也会改变了。所以为了保证y的值还是2.2,v8又做了一个re-boxed的操作,将2.2又另外装箱成了一个HeapNumber。通过这种方式来保证y的值不被改变。

说了这么多,怎么感觉离问题越来越远了 那让我们来结合一下上面提到的知识分析一下问题。


整理一下

const node1 = { actualStartTime: 0 };
const node2 = { actualStartTime: 0 };

还是这个简化后的代码,它们的Shape是什么样的呢?actualStartTime的初始值为0,是31位有符号整数范围内的小整数。number是SMI还是Double类型也和writable、configurable、enumerable一样会作为属性信息。所以如图:

795c78881406331eac1cc612a5027593.png
在之前图的基础上自己改的有点奇怪请不要介意 ‍♀️

但如果属性值分别为SMI和Double类型

const node1 = { actualStartTime: 0 };
const node2 = { actualStartTime: 0.2 };

比如这样,就不能合并成一个Shape.

而如果是

const node1 = { actualStartTime: 0 };
node.actualStartTime = 0.3;

旧的Shape会失效,如果没有其他对象指向它则之后会被垃圾回收,node1指向新的Shape.

61890dda5923296a86167455256cba11.png
在之前图的基础上自己改的有点奇怪请不要介意 ‍♀️

回到最初的起点 ‍♀️

class FiberNode {   
  constructor() {     
     this.actualStartTime = 0;     
     Object.preventExtensions(this);   
  } 
} 
const node1 = new FiberNode(); 
const node2 = new FiberNode();

而回到最初的代码,在Object.preventExtensions后大概变成这样

a36ca607e9e3b923f91f20922e51ad9f.png
图源见参考资料

到目前为止还没有什么问题,但当 node1.actualStartTime = performance.now() 的时候,

从表面来看Object.preventExtensions不允许对象添加新的属性,但是可以修改actualStartTime的值。

但在引擎内部,它就迷惑了。

82e5e2f1a18ca019e2f7775d083b135f.png

明明说好了不可扩展,这里怎么又要扩展出新的Shape了❓于是v8不知道要如何处理这种情况,于是就新建了一个独立的Shape,如图:

0ce8a577573feb55cf8437c299b08cf7.png
图源见参考资料

当fiberNode越来越多,这样的独立的Shape也愈来愈多,最终造成了性能问题。

想要解决这个问题,可以在初始化的时候就让actualStartTime等于一个double类型的值。

bbf165ed4c033586e14c8559fc869b03.png
来自上面提到的issue14365

不过v8在7.4版本里也修复了这个问题,对这种情况进行了正确的处理。

bfc7c018ede32be5da66d8356f5f6034.png
图源见参考资料

不会都再形成一个独立的Shape。而是如图一样能形成正确的Transition 链,没有被引用的Shape也能正常的进行GC,也不会引发性能问题。

3d476b88dfa2246a985128f89f9134bb.png
图源见参考资料

虽然这个问题最后被官方解决了,不过在这个过程中,也能了解到v8引擎对number和对象的以下幕后的处理。

写的比较仓促如果有问题欢迎指正和补充

参考资料

[1] https://v8.dev/blog/react-cliff

[2] JavaScript engine fundamentals: Shapes and Inline Caches

[3] https://v8.dev/blog/pointer-compression

[4] Fast propertiesin V8

[5] Elements kindsin V8

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值