使用构造函数也能带来性能提升?

506 阅读5分钟

作者:elecat
csdn

如果你非常注重性能,那么下面的代码可能对你很重要。

function Article() { 
	this.title = 'Inauguration Ceremony Features Kazoo Band'; 
}
let a1 = new Article();
let a2 = new Article();

构造函数跟性能有什么关系呢?这就要从js引擎(Chrome V8)的工作原理说起了——

使用隐藏类HiddenClasses

运行期间,V8会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好,V8会针对这种情况进行优化 ——《Javascript高级程序设计》

什么是隐藏类?

This HiddenClass stores meta information about an object, including the number of properties on the object and a reference to the object’s prototype. HiddenClasses are conceptually similar to classes in typical object-oriented programming languages. However, in a prototype-based language such as JavaScript it is generally not possible to know classes upfront. Hence, in this case V8, HiddenClasses are created on the fly and updated dynamically as objects change. HiddenClasses serve as an identifier for the shape of an object and as such a very important ingredient for V8's optimizing compiler and inline caches. The optimizing compiler for instance can directly inline property accesses if it can ensure a compatible objects structure through the HiddenClass —— v8 docs

HiddenClass 存储了一个对象的元数据,包括对象和对象引用原型的数量。HiddenClasses 在典型的面向对象的编程语言的概念中和“类”类似。然而,在像 JavaScript 这样的基于原型的编程语言中,一般不可能预先知道类。因此,在这种情况下,在 V8 引擎中,HiddenClasses 创建和更新属性的动态变化。HiddenClasses 作为一个对象模型的标识,并且是 V8 引擎优化编译器和内联缓存的一个非常重要的因素。通过 HiddenClass 可以保持一个兼容的对象结构,这样的话实例可以直接使用内联的属性。

关于 HiddenClasses 的基本假设是对象具有相同的结构,例如,相同的顺序对应相同的属性,则共用相同的 HiddenClass。当我们给一个对象添加属性或删除属性时,将产生新的HiddenClasses:

var o = {}
o.a = 'foo'
o.b = 'bar'
o.c = 'baz'

所以,实际开发中,应尽量做到在构造函数中全量覆盖所有可能出现的属性。

function MyObject(valueA,valueB,valueC) {
	this.a = valueA
	this.b = valueB
	this.c = valueC
}

let o1 = new MyObject('foo','bar','baz')
let o2 = new MyObject('foo1','bar1','baz1')

//good,use one of the same HiddenClass

同时,避免出现先创建后添加

function MyObject(value) {
	this.a = value
}

let o = new MyObject('foo')
o.b = 'newValue' //bad,create new HiddenClasses

先创建后删除

function MyObject(valueA,valueB) {
	this.a = valueA
	this.b = valueB
}

let o = new MyObject('foo','bar')
delete o.b //bad,create new HiddenClasses

顺序不同

function MyObject(valueA,valueB) {
	this.a = valueA
	this.b = valueB
}
function AnotherObject(valueA,valueB) {
	this.b = valueB
	this.a = valueA
}
let o = new MyObject('foo','bar')
let o2 = new AnotherObject('foo2','bar2')
//bad,because of the different sequence,o and o2 use different HiddenClasses

内联缓存Inline Caches(ICs)

同时,V8还使用内联缓存,借鉴某些汇编语言内存偏移量的机制,优化对象属性的读取、存储速度。

何为偏移量?

通俗来说就是:把整个内存地址分为若干段,每段都由一些存储单元构成,叫做内存段。内存段上的某个存储单元距离段首的距离,就叫做偏移量。

补充:段地址: cpu访问存储器时,地址寄存器所能访问的存储空间达不到地址总线所提供的范围,所以针对这种情况,就把内存地址分为若干段,用段地址(也叫逻辑地址,并不真实存在)表示各个内存段。实际地址: 也叫物理地址,内存中的内存单元实际地址。

优化原理

But where are these property attributes stored in memory? Should we store them as part of the JSObject? If we assume that we’ll be seeing more objects with this shape later, then it’s wasteful to store the full dictionary containing the property names and attributes on the JSObject itself, as the property names are repeated for all objects with the same shape. That’s a lot of duplication and unnecessarily memory usage. As an optimization, engines store the Shape of the object separately.

实际上在JS引擎中对象的属性名和属性值是分别存储的,属性值本身被按顺序保存在对象中,而属性名则建立一个列表(Shape),存储每个属性名的“偏移量(offset)”和其他描述符属性。

如果一个对象在运行时增加了新的属性,那么这个属性名单会过渡到一个新的Shape(只包含了新添加的属性)并链接回原Shape(原文中称为“过渡链”,transition chains),这样访问属性时如果最新的属性列表中没有找到,可以回溯到上一个列表去检索。

因为存在不同的对象有相同的属性名称列表而重用Shape,当它们发生不同改变会分别过渡到各自的新Shape,形成分叉结构(transition tree)。

但是如果频繁扩展对象使得Shape链非常长怎么办呢?引擎内部会针对这样的情况再整理一张表(ShapeTable),把所有属性名都列出来然后分别链接至它们所属的Shape...这看起来还是比较繁琐,但都是为了不要浪费“已经做过的工作”,使保留有用的检索信息——Inline Caches更加方便。

内联缓存与隐藏类结合

前面说过,隐藏类作为一个对象模型的标识,也是 V8 引擎优化编译器和内联缓存的一个非常重要的因素,存储对象属性名的列表(Shape)就保存在隐藏类里。

具有相似结构的对象,被同一个隐藏类所关联起来,在读取属性时,不再使用传统的HashTable方式,而改用偏移量机制直接去读取地址;对于高频(Hot)属性,还可以通过Inline Caches的过渡链(transition chains)减少检索次数,从而带来性能提升。

效果

使用如下两段代码,在相同环境下进行比对测试

//bad
function MyObject() {
	this.a = 0;
}

let o1 = new MyObject();
o1.b = 0 // 生成新的HiddenClass,且之前没有访问过,无内联缓存可用
console.time();
for (let i = 0; i < 1e7; i++) {
	o1.b = i;
}
console.timeEnd();
//good
function MyObject() {
	this.a = 0;
}

let o1 = new MyObject();
console.time();
for (let i = 0; i < 1e7; i++) {
	o1.a = i;
}
console.timeEnd();

测试结果如下(单位:ms):

badgood
25.03198242187518.5478515625
27.844970717.59179688
27.3569335918.62817383
27.460937520.23803711
27.213423418.65380859
26.1999511719.09399414
28.3391113318.97387695
27.3698730518.72119141
27.1879882819.63916016
27.3107910218.92382813
合计271.3159625189.0117188

如有错误,欢迎指正!

参考:

v8.dev/blog/fast-p…

mathiasbynens.be/notes/shape…

blog.csdn.net/Mart1nn/art…