Node.js 背后的 V8 引擎优化技术
Node.js 的执行速度远超 Ruby、Python 等脚本语言,这背后都是 V8 引擎的功劳。本文将介绍如何编写高性能 Node.js 代码。V8 是 Chrome 背后的 JavaScript 引擎,因此本文的相关优化经验也适用于基于 Chrome 浏览器的 JavaScript 引擎。
V8 引擎在虚拟机与语言性能优化上做了很多工作。按照 Lars Bak 的说法,所有这些优化技术都不 是他们创造的,只是在前人的基础上做的改进。
隐藏类 (Hidden Class)
为了减少 JavaScript 中访问属性所花的时间,V8 采用了和动态查找完全不同的技术实现属性的访问:动态地为对象创建隐藏类。这并不是什么新想法,基于原型的编程语言 Self 就用 map 来实现了类似功能。在 V8 中,当一个新的属性被添加到对象中时,对象所对应的隐藏类会随之改变。
例如,我们可以使用一个简单的 JavaScript 函数来加以说明:
function Point(x, y) {
this.x = x;
this.y = y;
}
当 new Point(x, y) 执行时,一个新的 Point 对象会被创建。如果这是 Point 对象第一次被创建,V8 会为它初始化一个隐藏类,不妨称作 CO。因为这个对象还没有定义任何属性,所以这个初始类是一个空类。
类转移信息
执行函数 Point 中的第一条语句 (this.x = x;) 会为对象 Point 创建一个新的属性 x。此时,V8 会在 CO 的基础上创建另一个隐藏类 C1,并将属性 x 的信息添加到 C1 中:这个属性的值会被存储在距 Point 对象偏移量为 0 的地方。
在 CO 中添加适当的类转移信息,使得当有另外的以其为隐藏类的对象在添加了属性 x 之后能找到 C1 作为新的隐藏类。此时对象 Point 的隐藏类更新为 C1。
执行函数 Point 中的第二条语句会添加一个新的属性 y 到对象 Point 中。同理,此时 V8 会有以下操作:
* 在 C1 的基础上创建另一个隐藏类 C2,並在 C2 中添加关于属性 y 的信息:这个属性将被存储在内存中的偏移量为 1 的地方。
* 在 C1 中添加适当的类转移信息,使得当有另外的以其为隐藏类的对象在添加了属性 y 之后能找到 C2 作为新的隐藏类。此时对象 Point 的隐藏类被更新为 C2。
避免隐藏类的创建
乍一看似乎每次添加一个属性都创建一个新的隐藏类非常低效。实际上,利用类转移信息,隐藏类可以被重用。下次创建一个 Point 对象时,就可以直接共享由最初那个 Point 对象所创建出来的隐藏类。
避免内存泄露
例如,当我们使用 setTimeout 函数时,我的对象 myObj 不会被释放掉,因为内部的 myRef 对象也指向了 myObj,而内部的 setTimeout 调用会将闭包加入 Node.js 事件循环的队列里,因此 myRef 对象不会释放。
使用数字的教训
V8 使用标记来高效地标识其值。V8 通过其值来推断你会以什么类型的数字来对待它。因为这些类型可以动态改变,所以一旦 V8 完成了推断,就会通过标记高效完成值的标识。不过有时改变类型标记还是比较消耗性能的,我们最好保持数字的类型始终不变,通常标识为有符号的 31 位整数是最优的。
使用 Array 的教训
为了掌控大而稀疏的数组,V8 内部有两种数组存储方式:
* 快速元素:对于紧凑型关键字集合,进行线性储存;
* 字典元素:对于其他情况,使用哈希表。
最好别导致数组存储方式的改变。