我们知道Javascript作为一种动态语言,性能方面与c#,Java之类的静态语言相比存在着一定的差距。而随着Web技术的发展,对Javascript的执行效率提出越来越高的要求。为了追求更好的性能,V8引擎借鉴了大量的静态语言编译技术来优化引擎的执行效率。比如V8引擎放弃生成中间字节码,而是直接从AST(抽象语法树)生成机器语言。与静态语言不同, javascript的程序在执行期间需要反复检查数据类型。因此,V8引擎中存在两种机制来优化这个过程。
hidden class 隐藏类
对于动态类型语言来说,由于类型的不确定性,在方法调用过程中,语言引擎每次都需要进行动态查询,这就造成大量的性能消耗,从而降低程序运行的速度。大多数的Javascript 引擎会采用哈希表的方式来存取属性和寻找方法。而为了加快对象属性和方法在内存中的查找速度,V8引擎引入了隐藏类(Hidden Class)的机制,起到给对象分组的作用。在初始化对象的时候,V8引擎会创建一个隐藏类,随后在程序运行过程中每次增减属性,就会创建一个新的隐藏类或者查找之前已经创建好的隐藏类。每个隐藏类都会记录对应属性在内存中的偏移量,从而在后续再次调用的时候能更快地定位到其位置。
function Person(name, age) {
this.name = name;
this.age = age;
}
var xiaoming = new Person("xiaoming", 32);
var lisi = new Person("lisi", 20);
xiaoming.email = "xiaoming@qq.com";
xiaoming.job = "teacher";
lisi.job = "chef";
lisi.email = "lisi@qq.com";
观察以上代码,当初始化Person
对象的时候, 最开始会创建一个C0的隐藏类,该类不带有任何属性。随后在调用构造器函数的时候,随着属性的增加,引擎会生成C1,C2的过渡隐藏类,隐藏类内部会记录属性的偏移量(offset)。之所以存在过渡隐藏类是为了在多个对象间能够共享隐藏类。
这里,注意到xiaoming
和lisi
两个对象使用的是同一个构造函数,所以它们会共享同一个隐藏类C2。随后虽然xiaoming
和lisi
两个对象都添加了job
和email
两个属性,但由于初始化顺序不同,会生成不同的隐藏类。
不同初始化顺序的对象,所生成的隐藏类是不一样的。因此,在实际开发过程中,应该尽量保证属性初始化的顺序一致,这样生成的隐藏类可以得到共享。同时,尽量在构造函数里就初始化所有对象成员,减少隐藏类的产生。
inline caching 内联缓存
仅拥有隐藏类似乎还不够,毕竟引擎在执行过程中还需要查找隐藏类。为了取得更好的性能,V8引擎加入了内联缓存(Inline Caching)技术来优化运行时查找对象及其属性的过程。这项技术其实很古老了,最初是应用在Smalltalk虚拟机上。核心原理就是在运行过程中,收集类型信息,从而可以让引擎在后续运行过程中利用这些类型信息作出预判。
对于动态查询优化来说,最简单的方式是利用缓存来保留最常使用的查询结果。每次调用对象上的方法或属性的时候先查询缓存,如果命中则直接使用缓存结果。如果未命中,就查询隐藏类来获取结果。内联缓存也是基于这个思想。但是如果想要进一步优化查询效率,应该怎么做呢? 考虑到在程序中类型很少发生改变,内联缓存技术会直接将查询结果写入调用方法中,来避免查询缓存。但是万一类型在程序执行中途发生变化了怎么办?对于这种情况,内联缓存会在直接调用之前验证类型,这些验证类型的代码叫做"前导代码"。
var arr = [1, 2, 3, 4];
arr.forEach((item) => console.log(item.toString());
像上面这段代码,数字1在第一次toString()
方法时会发起一次动态查询,并记录查询结果。当后续再调用toString
方法时,引擎就能根据上次的记录直接获知调用点,不再进行动态查询操作。
再来考虑下面这个情况:
var arr = [1, '2', 3, '4'];
arr.forEach((item) => console.log(item.toString());
可以看到,调用toString
方法的对象类型经常发生改变,这就会导致缓存失效。为了防止这种情况发生,V8引擎采用了 polymorphic inline cache (PIC) 技术, 该技术不仅仅只缓存最后一次查询结果,还会缓存多次的查询结果(取决于记录上限)。
参考资料
https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf
https://github.com/sq/JSIL/wiki/Optimizing-dynamic-JavaScript-with-inline-caches
https://richardartoul.github.io/jekyll/update/2015/04/26/hidden-classes.html