V8的高性能探秘
V8项目负责人Lars Bak:V8的所有优化都不是原创的。V8组合了过往对于动态语言的各种优化技术,因而具有了非常高效的性能。
快速属性访问
JavaScript是动态编程语言,这意味着可以动态的增加或删除对象的属性。
以往实现
大多数的JavaScript引擎都是使用类似字典的数据结构来保存一个对象的属性,在这种结构下每次访问一个属性都需要动态查找其在内存中的位置。
- 优势:
实现简单。
- 劣势:
效率低。相对于Java、Smalltalk等语言,由于对象的class有着固定的布局,因此属性在内存中有着由编译器生成相对于对象的固定位移,因此访问属性要快得多,可能仅需要一条机器指令。
V8的实现
为了减少访问属性的耗时,V8没有使用动态动态查找来访问属性。
V8为每个对象创建了一个隐藏类(Hidden Class),每当增加一个属性时更换新的隐藏类。
举例说明:
1 function Point(x, y) 2 { 3 this.x = x; 4 this.y = y; 5 }
当调用 new Point(x, y) 时产生一个新的Point对象,V8这样来处理:首先创建一个Point的隐藏类,在此例中假设叫C0,此时对象没有任何属性,初始时类是空的。在此阶段,Point对象的隐藏类为C0。
当执行Point里的第一条语句 this.x = x; 创建Point对象的一个新属性x。这时V8创建另外一个基于C0的隐藏类C1,然后在C1中增加描述信息,指明Point对象拥有属性x,并存储在相对于Point对象的位移0处。同时更新C0的类转移信息,指明当基于C0隐藏类的对象增加了属性x时,此对象的隐藏类将由C0替换为C1。在此阶段,Point对象的隐藏类为C1。
当执行Point里的第一条语句 this.y = y; 创建Point对象的一个新属性y。这时V8创建另外一个基于C1的隐藏类C2,然后在C2中增加描述信息,指明Point对象也拥有属性y,并存储在相对于Point对象的位移1处。同时更新C1的类转移信息,指明当基于C1隐藏类的对象增加了属性y时,此对象的隐藏类将由C1替换为C2。在此阶段,Point对象的隐藏类为C2。
无论何时只要增加一个属性就创建一个隐藏类,这种做法似乎看上去非常低效,然而由于类的转移信息,隐藏类可以被重用。想象一下这样的场景,当下一次一个新的Point对象被创建后,就不再需要创建新的隐藏类,新的Point对象将共享为第一个Point对象所创建的一系列隐藏类,具体过程如下:
初始化一个没有任何属性的Point对象,此时初始的隐藏类C0适用于此对象。
当增加属性x后,V8遵循从C0到C1的类转移信息,然后在C1指定的位移处写入x的值。
当增加属性y后,V8遵循从C1到C2的类转移信息,然后在C2指定的位移处写入y的值。
虽然JavaScript比大多数面向对象的语言更加动态,但绝大多数JavaScript程序的运行时行为导致了高度使用上述方法的共享结构。
- 优势:
不需要使用动态查找字典来访问对象的属性,尤其是当创建大量相同的对象时效率大幅提升。
可以让V8使用基于隐藏类的优化技术-内联缓存(Inline Caching)。
- 劣势:
实现略复杂。
每次对象新增属性都需要创建一个新的隐藏类,极端情况下效率非常低。
内联缓存&动态机器码生成
V8的内联缓存依托于隐藏类技术,这里的内联与C++中的内联是两个不同的概念。
当第一次执行JavaScript代码时V8直接将其编译成机器码,而不产生中间的字节码。(*Google已经发布了新的JavaScript解释器Ignition,旨在减少内存消耗,在一些内存有限的设备上使用)
在第一次指定访问一个给定对象的属性的代码时,V8会确定此对象的隐藏类。V8预测未来在此代码段上执行的所有对象属性访问都拥有相同的隐藏类,并将属性访问的代码通过使用此隐藏类,打进使用内联缓存访问指令的补丁(通常即为一条对内存偏移地址的访问指令,也就是内联的概念)。如果V8预测命中,则大大优化了属性访问耗时;如果V8未命中,则将打进的补丁代码移除,重新创建隐藏类并访问属性。
这样在处理大量相同类型的对象并频繁创建和访问时(绝大多数JavaScript代码都是如此执行),效率将极大提升。
高效的垃圾回收实现
V8垃圾回收机制简介
V8在运行时自动回收不再需要使用的对象内存,也即是垃圾回收。
V8使用了全暂停式(stop-the-world)、分代式(generational)、精确(accurate)等组合的垃圾回收机制,来确保更快的对象内存分配、更短的垃圾回收时触发的暂停以及没有内存碎片。
V8的垃圾回收有如下几个特点:
- 当处理一个垃圾回收周期时,暂停所有程序的执行。
- 在大多数垃圾回收周期,每次仅处理部分堆中的对象,使暂停程序所带来的影响降至最低。
- 准确知道在内存中所有的对象及指针,避免错误地把对象当成指针所带来的内存泄露。