Hidden Classes
Javascript,众所周知是一门动态类型语言,也就是说当一个对象被实例化之后,我们仍然可以随意的添加或者删除它的属性。例如,下面的代码中,我们实例化了一个car
,包含有make
和model
两个属性,同时,当car
被实例化了之后我们仍可以将其他属性year
赋予它。
```javascript
var car = function(make,model) {
this.make = make;
this.model = model;
}
var myCar = new car(honda,accord);
myCar.year = 2005;
大多数的JavaScript解释器通过类字典(dictionary-like,基于hash的)型的对象来存储对象中的属性。这样,相对于非动态类型语言(比如Java),这种结构的数据类型让我们在获取对象属性时会消耗更多的时间。在Java中,所有的对象属性会在编译之前就被一个固定的对象层所决定,同时在运行时不可修改。因此,这些属性的值(或者指向属性的指针)可以一个接一个的连续得存储在内存中,每一个属性都有一个固定的偏移量。偏移量可以简单的通过属性的类型决定,显而易见,由于JavaScript是一门动态类型语言,所以无法在运行时确定偏移量。
在非动态类型语言中,例如Java只需要一条指令就可以确定一个属性在内存中的位置,然而JavaScript则需要多条指令来从Hash Table中获得位置。这就导致在JavaScript中属性查找通常会比其他语言慢很多。
鉴于基于字典的存储方式非常的低效,V8采用了一个完全不同的方式:Hidden Class。除了JavaScript是在运行时完成一系列操作之外,Hidden Class的工作原理和Java中对待对象的处理方式很类似。在阅读剩下的文章时,请记住V8会给每一个对象都赋予一个Hidden Class,Hidden Class的终极目的就是优化属性的查询时间。现在我们来具体看看。
function Point(x,y) {
this.x = x;
this.y = y;
}
var obj = new Point(1,2);
一旦一个新的function被声明,JavaScript就会紧接着创建一个Hidden Class C0
。
由于暂时还没有属性,所以C0
是空的。
当执行到this.x = x
时,V8会创建第二个Hidden Class叫做C1
,C1
是基于C0
的。C1
描述了如何找到属性x
,也就是x
在内存中的位置。在这个例子中,简化为将x
存在偏移值为0的位置,这表明当我们在内存中查看Point
对象时,第一个偏移所在的位置会保存属性x
。V8同时会用class transition
将C0
更新,简单来说就是Hidden Class现在切换为C1
了。换句话说,C1
就变成了Point
对象的Hidden Class。
每当对象有新属性加入时,旧的Hidden Class就会通过一个过渡路径(transition path)更新为新的Hidden Class。这种过渡非常重要,因为这保证了用相同方式创建的两个对象可以共享同一个Hidden Class。如果有两个对象共享同一个Hidden Class,同时又有一个属性同时添加给了这两个对象,则这种过渡操作保证了新的Hidden Class仍然是这两个对象的Hidden Class。
当this.y = y
执行时,重复之前的操作。一个新的叫做C2
的Hidden Class被创建,一个过渡会添加给C1
,同时Hidden Class转换为C2
,Point
对象的Hidden Class现在就被更新为了C2
。
注意:Hidden Class的过渡取决于属性被添加的顺序。
function Point(x,y) {
this.x = x;
this.y = y;
}
var obj1 = new Point(1,2);
var obj2 = new Point(3,4);
obj1.a = 5;
obj1.b = 10;
obj2.b = 10;
obj2.a = 5;
到var obj2 = new Point(3,4);
为止,obj1和obj2共享同一个Hidden Class。但是,由于属性a
和b
在之后被添加的顺序不同,导致了Hidden Class过渡时走向和不同的路径。
通过上述的例子,也许你直观上会觉得obj1和obj2拥有不同的Hidden Class并没有什么关系。因为每一个Hidden Class都保存着合适的偏移量,那么不管obj1和obj2有没有共享同一个Hidden Class,他们获取属性的速度应该是一样的。为了理解为什么这样的想法是错的,让我们来学习一个V8中的优化技术,叫做内联缓存(inline caching)。
内联缓存
V8利用了一个在动态类型语言中常用的优化技巧,内联缓存。简单来说,内联缓存依赖于同类型对象的同一方法的重复调用。
那么这是怎么实现的呢?V8维护了一个缓存——最近被调用的函数中,被作为参数传递的对象类型,然后利用这些信息并假设这种对象类型在将来仍然会被作为参数传递。如果V8能正确的做出这种预测,那么它就能绕过寻找对象属性的过程,直接从对象的Hidden Class中寻找之前存好的信息。
好的,那么Hidden Class的思想是这么和内敛缓存相结合起来的呢?当一个特定的对象的方法被调用时,V8引擎需要利用对象的Hidden Class来决定用哪个偏移量来寻找属性。当针对同一个Hidden Class的方法被成功调用两次,V8就会跳过Hidden Class的属性寻找,直接将属性的偏移量绑定到对象的指针上。当该方法再次被调用时,V8引擎假定Hidden Class没有被修改,我们可以直接用上次在Hidden Class中查找到的属性偏移量来去内存中寻找具体的值。这大大提高了执行的速度。
这就是为什么属性的添加顺序显得很重要了。如果属性的添加顺序不同,两个对象的Hidden Class就不同,也就无法使用内敛缓存优化了。
当然,鉴于JavaScript仍然是门动态类型语言,所以总会有判断失误的时候。这时候就需要返回传统的方法,利用Hidden Class来获取属性的偏移量了。
一些优化技巧
- 实例化对象时永远保存属性的顺序相同。
- 给对象增加新的属性时,会导致Hidden Class更新,同时针对旧Hidden Class的优化失效,导致速度变慢。因此,尽量在构造函数中确定好对象的属性。
- 运行同一个方法多次会比运行多个方法一次要快的多(参照内联缓存)。