上一篇(JS引擎、运行时与调用栈概述)主要讲了JS引擎、运行时与调用栈的概述。本篇文章将会深刻到谷歌V8 JavaScript引擎的内核部分。咱们也会提供一些怎样写出更好的JavaScript代码的建议。javascript
一个JavaScript引擎是执行JavaScript代码的一个程序或者说一个解释器。一个JavaScript引擎能够实现成一个标准的解释器,或者以某种形式将JavaScript编译成字节码的即时编译器。html
如下是一个比较流行的实现JavaScript引擎的项目列表:java
由谷歌构建的V8引擎是用C++编写的开源项目,用于谷歌Chrome内部。然而不像其余引擎,V8也被用于流行的Node.js运行时。node
V8最开始是为了提升运行在浏览器内部的JavaScript运行性能而设计的。为了提升速度,V8将JavaScript代码转换成更有效率的机器码,而不是使用一个解释器。就像其余一些JavaScript引擎好比SpiderMonkey或Rhino (Mozilla)所作的同样,V8实现了一个即时(JIT)编译器在代码执行时将JavaScript代码编译成机器码。这里最主要的区别是V8不生成字节码或其余中间代码。git
在V8 5.9版本出来以前,引擎使用了两种编译器:github
V8引擎内部也使用了几个线程:web
最初执行JavaScript代码的时候,V8使用full-codegen直接将JavaScript转换成机器码而没作任何转化,这让引擎能很快开始执行代码。值得注意的是V8不使用中间字节码,那就不须要一个解释器了。编程
当你的代码运行一段时间后,性能分析线程收集到了足够的数据来告知哪些方法须要优化。数组
接下来,Crankshaft开始在另外一个线程优化了。它将JavaScript抽象语法树转换成一个叫作Hydrogen的高阶静态单赋值形式并试图优化这个Hydrogen图,大多数优化都是在这个阶段进行的。浏览器
第一步优化是提早内联尽量多的代码啊。内联是将调用地址(函数被调用的代码行)替换成被调用的函数体。这一简单的步骤是接下来的优化更有意义。
JavaScript是一门基于原型的语言:没有类,对象是使用克隆来构造的。JavaScript是一门动态编程语言,意味着属性能够在对象初始化完成后很容易的被添加或已移除。
大多数JavaScript解释器使用类字典结构(基于哈希函数)在内存中存储对象属性值的位置。这个结构使得在JavaScript中检索一个属性的值比在其余非静态语言如Java或C#须要更多的计算。在Java中,全部的对象属性在编译前就已经被一个固定的对象决定了,并且不能在运行时被动态添加或删除(好吧,C#有一个动态类型,而这是另一个话题了)。结果是,属性值(或者指向那些属性的指针)能够存储在内存中一个连续的缓冲区中,彼此之间偏移量是固定的。这个偏移的长度根据属性的类型能够很容易的肯定,然而在属性类型在运行时能够改变的JavaScript中是不可能的。
由于使用字典在内存中查找对象属性的位置是很低效的,V8使用一个不一样的方法来代替:隐藏类(hidden classes)。隐藏类很像在Java中使用的固定对象层(类),除了它们是在运行时建立的。如今让咱们来看一下它们是什么样的:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(1, 2);
复制代码
只要“new Point(1,2)”被执行,V8就会建立一个隐藏类叫作“C0”。
Point中尚未属性,因此“C0”是空的。
只要第一个表达式“this.x = x”被执行了(在“Point”函数内部),V8就会建立第二个叫作“C1”的基于“C0”的隐藏类。“C1”描述属性x在内存中的位置(关联对象指针)。在这里,“x”存储在偏移量为0的位置上,这意味着当把point对象在内存中看做连续的缓冲区,第一个偏移量指向的是属性“x”。V8也会用“类转换”来更新“C0”,代表若是属性“x”被加入到point对象中,隐藏类就会从 “C0”切换成“C1”。下图中point对象的隐藏类如今已是“C1”了。
每次有新的属性被加入到一个对象中,旧的隐藏类就会更新转换路径到新的隐藏类。隐藏类转换是很重要的,由于它们可让隐藏类在以相同方式建立的对象中共享。若是两个对象共享一个隐藏类,并且相同的属性被加入到它们中,转换就能保证两个对象得到相同的新隐藏类和随之携带的全部优化过的代码。
当表达式“this.y = y”被执行的时候(也在Point函数中,“this.x = x”表达式以后),会重复上述过程。
一个叫作“C2”的隐藏类被建立了,一个类型转换被加入到“C1”中,代表若是有属性y加入到Point对象中(已经包含属性“x”),那么隐藏类将会变为“C2”,point对象的隐藏类更新为“C2”。
隐藏类转换依赖于属性加入到对象中的顺讯,看一下以下代码片断:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(1, 2);
p1.a = 5;
p1.b = 6;
var p2 = new Point(3, 4);
p2.b = 7;
p2.a = 8;
复制代码
如今你可能会假定p1和p2会使用相同的隐藏类和转换。然而并非如此。对“p1”来讲,属性“a”先会被加入,而后是属性“b”。而“p2”,“b”先被赋值,而后是“a”。这样,“p1”和“p2”结果会使用不一样的隐藏类和不一样的转换路径。所以,最好是使用相同的顺序来初始化动态属性,那样隐藏类能够被重复使用。
V8利用另外一项叫作内联缓存的技术来优化动态类型语言。内联缓存依赖于发生在相同对象类型上的相同方法的重复调用。一个内联缓存的深度解释能够在这里找到。
咱们将会说一下内联缓存的通用概念(以防你没有时间去看上面的深度解释)。
内联缓存是如何工做的呢?V8维护一个在最近的屡次方法调用中传递的参数的对象类型的缓存,用这个信息来假设将来传递的参数的对象类型。若是V8能够很好的假设传递给方法的对象类型,它就能够绕过推测怎样访问对象属性的过程,取而代之,使用前面保存的信息来查找到对象的隐藏类。
那么隐藏类和内联缓存是怎样关联起来的呢?当调用一个特定对象的方法时,V8引擎必须查找到那个对象的隐藏类,来肯定访问特定属性的偏移量。当两次成功调用相同缓存类的相同方法以后,V8将会略过隐藏类的查找,只是将属性的偏移量加入到对象指针中。在之后那个方法的全部调用中,V8引擎都会假定隐藏类没有改变,而使用前面查找中存储的偏移量直接跳到那个特定属性的内存地址上。这大幅度的提升了执行速度。
内联缓存也是相同类型的对象共享内联缓存如此重要的缘由。若是你建立两个类型相同,隐藏类不一样的对象(就像以前的例子那样),V8将不会使用内联缓存,由于即便两个对象类型相同,但它们对应的隐藏类给它们的属性分配了不一样的偏移量。
两个对象基本相同,但“a”和“b”属性是以不一样的顺序建立的。
只要Hydrogen图被优化过了,Crankshaft就会将它降成叫作Lithium的低阶形式。大多数Lithium实现是特定结构的。注册内存分配发生在这个阶段。
最后Lithium被编译成机器码。而后叫OSR:栈上替换(on-stack replacement)的事情发生了。在咱们编译和优化一个明显运行很长时间的方法以前,咱们可能先要运行它。V8不会忘记刚刚运行缓慢的方法,会使用优化后的版原本运行它。取而代之,它将会转换咱们拥有的全部上下文(栈,注册器),那样咱们就能够在执行的中间替换成优化后的版本。这是一个很是复杂的任务,记住在其余优化中,V8在最开始就已经内联了代码。V8不是惟一一个这样作的引擎。
有个叫去优化的保护措施,用相反的转化将代码恢复成未优化的代码,以防引擎的假设再也不正确。
对于垃圾回收,V8使用了传统的标记-清除分代方法来清除老生代。标记阶段会中止JavaScript的执行。为了控制GC消耗,使执行更加稳定,V8使用递增标记:不遍历整个堆,而是试图标记可能的对象,它只遍历堆的一部分,而后恢复正常的执行。下一次GC将会从上次堆遍历中止的地方开始。这只会形成在正常执行中的很短暂的暂停。就像前面提到的,清除阶段将会由另外的线程处理。
在2017年发布的V8 5.9版本,引进了一个新的执行管道。这个新的管道在真实的JavaScript项目中取得了更加大的性能改善和显著的内存节省。
这个新的管道构建在Ignition, V8的解释器和TurboFan,V8最新的优化编译器的顶上。
你能够在这里看到V8团队关于这个主题的博客。
自从V8的5.9版本发布依赖,full-codegen和Crankshaft(自从2010年就开始服务于V8的技术)就再也不被V8做为JavaScript的执行所使用,由于V8团队须要尽力跟上新的JavaScript语言特性和知足这些特性的优化须要。
这意味着整体上V8在将来将会拥有简单的多和可维护度更高的体系结构。
这些改善只是开始。新的Ignition和TurboFan管道为未来的优化铺好了道路,这样会大幅度提升JavaScript的性能,使V8在接下来的许多年在Chrome和Node.js上踩下更坚实的足迹。
最后,这里有一些怎样写出良好优化的更好的JavaScript的一些建议和技巧。你能够很容易根据上面的内容得出这些结论,这里只是为了你的方便,总结一下:
怎样写出性能优化的代码