V8 Javascript 引擎设计理念
转自http://blog.pluskid.org/?p=186
本文翻译自Google的开源Javascript引擎V8的在线文档。其实我都没有真正翻译过什么东西,本来我的英文就比较一般,中文语言组织也很弱。而且许多文档(比如这篇)基本上如果是对此感兴趣的人,直接阅读英文原文文档肯定都是没有问题的。不过既然突然心血来潮,就试一试吧,能力总是要锻炼才会有的。我自己对LanguageVM比较感兴趣,V8其实并不是一个VM,因为它是直接编译为本地机器码执行的,但是也有不少相通的地方。废话少说,下面是译文。
NetscapeNavigator在90在年代中期对JavaScript进行了集成,这让网页开发人员对HTML页面中诸如form、frame和image之类的元素的访问变得非常容易。由此JavaScript很快成为了用于定制控件和添加动画的工具,到90年代后期的时候,大部分的JavaScript脚本仅仅完成像“根据用户的鼠标动作把一幅图换成另一幅图”这样简单的功能。
随着最近AJAX技术的兴起,JavaScript现在已经变成了实现基于web的应用程序(例如我们自己的Gmail)的核心技术。JavaScript程序从聊聊几行变成数百KB的代码。JavaScript被设计于完成一些特定的任务,虽然JavaScript在做这些事情的时候通常都很高效,但是性能已经逐渐成为进一步用JavaScript开发复杂的基于web的应用程序的瓶颈。
V8是一个全新的JavaScript引擎,它在设计之初就以高效地执行大型的JavaScript应用程序为目的。在一些性能测试中,V8比InternetExplorer的JScript、Firefox中的SpiderMonkey以及Safari中的JavaScriptCore要快上数倍。如果你的web程序的瓶颈在于JavaScript的运行效率,用V8代替你现在的JavaScript引擎很可能可以提升你的程序的运行效率。具体会有多大的性能提升依赖于程序执行了多少JavaScript代码以及这些代码本身的性质。比如,如果你的程序中的函数会被反复执行很多遍的话,性能提升通常会比较大,反过来,如果代码中有很多不同的函数并且都只会被调用一次左右,那么性能提升就不会那么明显了。其中的原因在你读过这份文档余下的部分之后就会明白了。
V8的性能提升主要来自三个关键部分:
快速属性访问
动态机器码生成
高效的垃圾收集
快速属性访问
JavaScript是一门动态语言,属性可以在运行时添加到或从对象中删除。这意味着对象的属性经常会发生变化。大部分JavaScript引擎都使用一个类似于字典的数据结构来存储对象的属性,这样每次访问对象的属性都需要进行一次动态的字典查找来获取属性在内存中的位置。这种实现方式让JavaScript中属性的访问比诸如Java和Smalltalk这样的语言中的成员变量的访问慢了许多。成员变量在内存中的位置离对象的地址的距离是固定的,这个偏移量由编译器在编译的时候根据对象的类的定义决定下来。因此对成员变量的访问只是一个简单的内存读取或写入的操作,通常只需要一条指令即可。
为了减少JavaScript中访问属性所花的时间,V8采用了和动态查找完全不同的技术来实现属性的访问:动态地为对象创建隐藏类。这并不是什么新的想法,基于原型的编程语言Self就用map来实现了类似的功能(参见AnEfficientImplementationofSelf,aDynamically-TypedObject-OrientedLanguageBasedonPrototypes)。在V8里,当一个新的属性被添加到对象中时,对象所对应的隐藏类会随之改变。
下面我们用一个简单的JavaScript函数来加以说明:
functionPoint(x,y){
this.x=x;
this.y=y;
}
当newPoint(x,y)执行的时候,一个新的Point对象会被创建出来。如果这是Point对象第一次被创建,V8会为它初始化一个隐藏类,不妨称作C0。因为这个对象还没有定义任何属性,所以这个初始类是一个空类。到这个时候为止,对象Point的隐藏类是C0。
map_trans_a
执行函数Point中的第一条语句(this.x=x;)会为对象Point创建一个新的属性x。此时,V8会:
在C0的基础上创建另一个隐藏类C1,并将属性x的信息添加到C1中:这个属性的值会被存储在距Point对象的偏移量为0的地方。
在C0中添加适当的类转移信息,使得当有另外的以其为隐藏类的对象在添加了属性x之后能够找到C1作为新的隐藏类。此时对象Point的隐藏类被更新为C1。
map_trans_b
执行函数Point中的第二条语句(this.y=y;)会添加一个新的属性y到对象Point中。同理,此时V8会:
在C1的基础上创建另一个隐藏类C2,并在C2中添加关于属性y的信息:这个属性将被存储在内存中离Point对象的偏移量为1的地方。
在C1中添加适当的类转移信息,使得当有另外的以其为隐藏类的对象在添加了属性y之后能够找到C2作为新的隐藏类。此时对象Point的隐藏类被更新为C2。
map_trans_c
咋一看似乎每次添加一个属性都创建一个新的隐藏类非常低效。实际上,利用类转移信息,隐藏类可以被重用。下次创建一个Point对象的时候,就可以直接共享由最初那个Point对象所创建出来的隐藏类。例如,如果又一个Point对象被创建出来了:
一开始Point对象没有任何属性,它的隐藏类将会被设置为C0。
当属性x被添加到对象中的时候,V8通过C0到C1的类转移信息将对象的隐藏类更新为C1,并直接将x的属性值写入到由C1所指定的位置(偏移量0)。
当属性y被添加到对象中的时候,V8又通过C1到C2的类转移信息将对象的隐藏类更新为C2,并直接将y的属性值写入到由C2所指定的位置(偏移量1)。
尽管JavaScript比通常的面向对象的编程语言都要更加动态一些,然而大部分的JavaScript程序都会表现出像上述描述的那样的运行时高度结构重用的行为特征来。使用隐藏类主要有两个好处:属性访问不再需要动态字典查找了;为V8使用经典的基于类的优化和内联缓存技术创造了条件。关于内联缓存的更多信息可以参考EfficientImplementationoftheSmalltalk-80System这篇论文。
动态机器码生成
V8在第一次执行JavaScript代码的时候会将其直接编译为本地机器码,而不是使用中间字节码的形式,因此也没有解释器的存在。属性访问由内联缓存代码来完成,这些代码通常会在运行时由V8修改为合适的机器指令。
在第一次执行到访问某个对象的属性的代码时,V8会找出对象当前的隐藏类。同时,V8会假设在相同代码段里的其他所有对象的属性访问都由这个隐藏类进行描述,并修改相应的内联代码让他们直接使用这个隐藏类。当V8预测正确的时候,属性值的存取仅需一条指令即可完成。如果预测失败了,V8会再次修改内联代码并移除刚才加入的内联优化。
例如,访问一个Point对象的x属性的代码如下:
point.x
在V8中,对应生成的机器码如下:
;ebx=thepointobject
cmp[ebx,<hiddenclassoffset>],<cachedhiddenclass>
jne<inlinecachemiss>
moveax,[ebx,<cachedxoffset>]
如果对象的隐藏类和缓存的隐藏类不一样,执行会跳转到V8运行系统中处理内联缓存预测失败的地方,在那里原来的内联代码会被修改以移除相应的内联缓存优化。如果预测成功了,属性x的值会被直接读出来。
当有许多对象共享同一个隐藏类的时候,这样的实现方式下属性的访问速度可以接近大多数动态语言。使用内联缓存代码和隐藏类实现属性访问的方式和动态代码生成和优化的方式结合起来,让大部分JavaScript代码的运行效率得以大幅提升。
高效的垃圾收集
V8会自动回收不再被对象使用的内存,这个过程通常被称为“垃圾收集(GarbageCollection)”。为了保证快速的对象分配和缩短由垃圾收集造成的停顿,并杜绝内存碎片,V8使用了一个stop-the-world,generational,accurate的垃圾收集器,换句话说,V8的垃圾收集器:
在执行垃圾回收的时候会中断程序的执行。
大部分情况下,每个垃圾收集周期只处理整个对象堆的一部分,这让程序中断造成的影响得以减轻。
总是知道内存中所有的对象和指针所在的位置,这避免了非accurate的垃圾收集器中普遍存在的由于错误地把对象当作指针而造成的内存溢出的情况。
在V8中,对象堆被分成两部分:用于为新创建的对象分配空间的部分和用于存放在垃圾收集周期中生存下来的那些老的对象的部分。如果一个对象在垃圾收集的过程中被移动了,V8会更新所有指向这个对象的指针到新的地址。