V8 Design Elements(翻译)

最近开始学习V8 Javascript引擎,这篇文章是翻译官方文档的,解释了V8之所以快的主要原因等,原文请参见http://code.google.com/apis/v8/design.html。

V8是一个新的为了提高Javascript程序速度的Javascript引擎,在多项测试中,V8的速度比JScript (in Internet Explorer), SpiderMonkey (in Firefox), and JavaScriptCore (in Safari)都要快很多倍,如果你的JS网络应用程序正受制于JS引擎的速度,那么使用V8而不是你现在使用的JS引擎将可以大幅改善你的应用程序的表现。至于提升的幅度则由JS所占的比例以及JS的结构(nature of JS)等多方面因素决定,例如,如果一个函数在你的应用中将会被一次次重复运行,那么提升的幅度将会比很多函数都在你的应用中只运行一次要大很多,至于会这样的原因将会在后面的文章中解释。

V8速度提升的三个主要方面为:

  • Fast Property Access (快速的属性访问)
  • Dynamic Machine Code Generation (动态生成机器码)
  • Efficient Garbage Collection (高效的垃圾回收)


快速的属性访问

JS是一种动态语言,对象的属性(property)可以在运行时被动态的添加和删除,这意味着对象的属性很有可能被改变,绝大多数JS引擎采用一个字典型的数据结构来存储对象的属性,每一次属性的访问都需要一次动态的查询以获得属性在内存中的位置,这样的访问方法使得JS中的属性访问要比普通编程语言例如Java和Smalltalk中的对象访问要慢很多,在这些(普通的)编程语言中,对象的属性值根据类的固定结构,被编译器放在离对象指针有固定的偏移值的内存位置上,属性的访问只是一次简单的内存读取和写入,通常只需要一个(汇编)指令。

为了减少访问JS属性的时间,V8没有采用动态查询的方式,V8会在背后动态的创建隐藏类,在V8中,当对象的属性改变时,对象会更改隐藏类的指向。这种方式的基本思想并不是被创新出来的,在prototype-based的编程语言Self中也做了类似的事情,详见An Efficient Implementation of Self, a Dynamically-Typed Object-Oriented Language Based on Prototypes。

我们通过举例来更加清楚的了解整个过程。请看下面这个很简单的JS函数。

function Point(x, y) {
    this.x = x;
    this.y = y; 
}

当new Point(x, y)被执行的时候,一个新的Point对象会被创建,当V8第一次执行这个函数的时候,V8会为Point创建一个初始的隐藏类,为方便,我们假设这个类叫C0,显然这个类里面什么都没有,这个时候,Point这个对象的隐藏类是C0。

V8 Design Elements(翻译)

当执行Point中的第一个语句(this.x = x;),会在Point对象中创建一个新的属性x,对于V8而言,会做如下事情:

  • 创建另外一个隐藏类C1,继承自C0,同时在C1中加入属性x,x的值会被存在离Point对象偏移0的地方。
  • 更新C0的描述,添加一个类转移,以后,如果一个隐藏类指向C0的对象中加入了x属性,那么这个对象的隐藏类将会指向C1(这样可以避免每次加入新属性的时候都需要重新创建一个新的隐藏类,如果我们第二次调用Point函数,就不会再次创建C1和C0类,而会直接使用这次创建的,这个也说明了为什么如果一个函数在应用中被重复运行的话,那么速度的提升就会很大)。在这个时候,Point对象的隐藏类指向C1

V8 Design Elements(翻译)


当执行Point中的第二个语句(this.y = y;)的时候,将会在Point对象中创建一个新的属性y,对于V8而言,会做如下事情:

  • 创建一个新的隐藏类C2,继承自C1,同时对C2加入一个属性y,存储在离Point对象便宜1的内存位置上。
  • 更新C1的描述,添加一个类转移,以后,如果一个隐藏类指向C1的对象中加入了y属性,那么这个对象的隐藏类将会指向C2.

V8 Design Elements(翻译)

每次有一个属性被添加的时候就要重新创建一个隐藏类的做法看起来好像很低效,但是,因为类转移的存在,隐藏类可以被复用(如我上面所解释)。尽管JS比其他面向对象的编程语言都要更加动态,但是采用上面的方法,通过观察很多JS程序的运行,它们在很大程度上都重用了之前的结构(the runtime behavior of most JS programs results in a high degree of structure-sharing using the above approach). 利用隐藏的方法有两个好处:属性的访问不再需要一个字典查询,同时可以让V8使用一些在以类为基础的普通编程语言中可以用的优化方法,例如inline caching(参考 Efficient Implementation of the Smalltalk-80 System)。

动态生成机器码

V8会JS代码第一次运行的时候将其直接编译为机器码,在V8中没有中间字节码,也就没有解释器,属性的访问由inline cache来完成,但这些代码可能会在V8执行期间被更改为别的机器指令。

当第一次执行访问某个对象的某个属性的代码的时候,V8决定这个对象现在的隐藏类,同时会进行如下优化,V8假设当前代码块中的对这个对象的所有的属性访问都会使用这个隐藏类,并根据这个假设修改inline cache的代码,直接使用这个隐藏类(跳过查询隐藏类的步骤),如果V8的假设是正确的,那么属性值的读取和赋值只需一个指令即可完成,如果假设错误,那么V8再次修改代码,移除这一优化。

举例,JS中访问一个Point对象的x属性的代码为

point.x

在V8中,相对应的机器码为

# ebx = the point object
cmp [ebx,<hidden class offset>],<cached hidden class>
jne <inline cache miss>
mov eax,[ebx, <cached x offset>]

如果这个对象的隐藏类不符合缓存代码中的隐藏类,执行将会跳转到V8运行系统中处理inline cache失败的地方并修改inline cache的代码,如果找到了这个属性,那么x的值就会直接被得到。

当有许多对象共享同一个隐藏类的时候,这样的方法能够使得JS的属性访问速度和大部分静态语言的访问速度相仿,使用隐藏类并通过inline cache代码来访问属性,同时优化机器码的生成,V8能够大幅优化大部分JS代码的执行效率。

高效的垃圾回收

V8会将不再被引用的内存进行回收,这个过程通常被称之为垃圾回收,为了保证快速的对象生成,缩短垃圾回收所造成的暂停,并且防止内存碎片的产生,V8的垃圾回收器使用了如下原则:stop-the-world, generational, accurate。具体来说:

  • 当处在垃圾回收循环中时,停止程序的执行
  • 在大多数垃圾回收循环中仅仅处理一部分的堆内存,这样做最小化了程序停止所带来的影响
  • 永远知道所有对象和指针在内存中的位置,这个避免了不正确的垃圾回收器中普遍存在的内存泄露的问题。

在V8中,对象堆内存被分为两部分,用于新对象创建的新的内存空间,用于存放在垃圾回收周期中存留下来的对象,如果一个对象在垃圾回收中被移动了,V8会更新所有指向改对象的指针。

你可能感兴趣的:(V8)