前一篇文章JavaScript深层工作原理解析(一)中,我们概览了js运行的引擎、运行环境、调用栈。
本篇将深入google的V8引擎,随后会讲到如何书写性能最优的代码。(想要直接看结果的朋友直接看最后的技巧建议便可)
一、概览
一个Javascript引擎就是一个程序(一个执行js代码的解释器)
下面是一个实现了js引擎的项目列表:
- V8 — Google开源引擎,由C++编写;
- Rhino — Mozilla开源并管理着,由Java编写;
- SpiderMonkey — 第一个js引擎, 在Netscape Navigator那个时代的产物;
- JavaScriptCore — webkit的一个重要组成部分,主要是对JS进行解析和提供执行环境;08年WebKit项目重写了JavaScriptCore,Apple应用在了Safari上;
- KJS — KDE项目的Konqueror web浏览器引擎;
- Chakra (JScript9) — Internet Explorer
- Chakra (JavaScript) — Microsoft Edge
- Nashorn — OpenJDK内的开源引擎,由Oracle Java Languages and Tool Group编写;
- JerryScript — 一个为Internet of Things而生的引擎。
二、为何创造V8引擎
Google用C++编写创造了V8引擎并开源,它被用于Chrome内,同时还用于Nodejs。
V8是第一个设计用来提高js在web浏览器的执行性能的引擎。为了获得更好的js执行速度,V8将JavaScript代码转换为更高效的机器代码,而不是使用解释器。它通过实现一个JIT(即时)编译器,就像许多现代的JavaScript引擎一样,像SpiderMonkey或Rhino(Mozilla)一样,将JavaScript代码编译成机器代码。这里的主要区别是V8没有生成字节码或任何中间代码。
三、V8的编译器及线程
在5.9版本之前,V8有2个编译器:
- full-codegen — 一个简单快速的编译器,它产出简单、相对而言速度略慢的机器代码;
- Crankshaft — 一个运行时优化过的编译器,相对而言会产出更高效的代码。
V8引擎内部开启了多线程:
- 主线程运行、编译、执行代码;
- 还有一个用于编译的单独的线程,以便主线程可以继续执行,而前者是在优化代码;
- 一个分析器线程,它将告诉运行时我们花了很多时间,以便Crankshaft优化它们;
- 处理垃圾收集器清理的几个线程。
内部具体如何配合的,如下:
在第一次执行JavaScript代码时,V8利用了full-codegen编译器,该代码直接将解析后的JavaScript转换为机器代码,而无需进行任何转换。这使得它可以非常快地开始执行机器代码。注意,V8并没有使用中间字节码表示,这样可以消除对解释器的需要。
当您的代码运行了一段时间后,分析器线程已经收集了足够的数据来判断应该对哪个方法进行优化。
接下来,Crankshaft编译器从另一个线程开始对代码进行优化。大多数优化都是在这个级别完成的。
四、关联组合代码
第一个优化是预先内联尽可能多的代码。内联就是把调用函数的地方替换成函数体代码执行结果。这个简单的步骤允许以下的优化更有意义。
js是一门基于原型链(prototype)的语言,它也是一门动态编程语言,意味着可以很轻易的从一个实例化的对象上添加或者删除各种属性。
大多数JavaScript解释器使用类字典的结构(基于哈希函数)来存储对象属性值在内存中的位置。这种类字典结构在检索属性时的性能,相较于静态类型语言(比如Java、C#)会更昂贵(开销大)。在Java中,所有对象属性都是在编译之前由固定的对象布局(fixed object layout)决定的,在运行时不能动态添加或删除,我们知道,在存储不同类型的值的时候需要分配的内存大小和地址是不一样的,因此一开始就定义好该对象属性的类型是什么,就能在一开始就确定好该类型需要在内存中占据的空间。
接下来让我们看看V8如何通过类字典来快速的在内存中查找出对象的属性:
这是一个示例:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(1, 2);
当“ new Point(1,2) ”被调用时,V8将会创建一个被称为“C0”的隐藏类(hidden class),C0很类似Java这类强类型语言的class。一开始因为该C0内什么都没有,就是空的。
当执行Point函数内部第一行代码this.x时,V8将会创建第二个隐藏类(hidden class),被称作“C1”。(C1用来描述属性x在内存中的位置信息)在这种情况下,“x”存储在偏移量0中(offset 0),这意味着当将内存中的一个点对象视为连续的缓冲区时,第一个偏移量将对应于属性“x”。V8还将使用一个“类转换”更新“C0”,它声明如果将一个属性“x”添加到一个point对象中,隐藏类应该从“C0”切换到“C1”。下面的point对象的隐藏类现在是“C1”。
然后每当有新属性被添加进来时,就会从上一个隐藏类切换到当前属性对应创建的隐藏类。所以,当执行到this.y这第二行代码时,又为该对象创建了一个新属性y,所以V8就会创建出C2隐藏类,并将隐藏类切换到C2:
隐藏类传递切换的顺序依赖于对象内属性的添加顺序。
接下来说一下跟隐藏类相关的另一技术。V8利用了另一种优化动态类型语言的技术,称为内联缓存。内联高速缓存依赖于这样的观察,即对相同方法的重复调用往往发生在同一类型的对象上。这里可以找到对内联缓存的深入解释。
我们将讨论内联缓存的一般概念。
那么它是如何工作的呢?V8在最近的方法调用中将对象的类型缓存下来,并将该缓存下来的类型作为参数传递,并使用这些类型缓存信息对将来作为参数传递的这些对象的类型进行了提前假设。如果V8能够对将要传递给某个方法的对象类型做出一个良好的假设,那么它就可以绕过查找如何访问对象属性的过程,相反,可以使用以前查找的存储信息到对象的隐藏类,从而提高运行效率。
那么隐藏类和内联缓存的有什么联系呢?
每当调用某个特定对象的方法时,V8引擎就必须对该对象的隐藏类执行查找,以确定访问特定属性的偏移量。在两次成功调用同一个隐藏类的方法之后,V8省略了隐藏类查找,并简单地将属性的偏移量添加到对象指针本身。对于该方法的所有未来调用,V8引擎假设隐藏的类没有改变,并直接跳转到内存地址,从而提高了运行速度。
当执行时检查到内联缓存为之前相同的一个,就会采用之前的隐藏类来直接跳转内存地址。
当Crankshaft(文章开头提到过,一个运行时优化过的编译器)完成了优化,Crankshaft把它降低到一个叫做Lithium的低级表示形。大多数的Lithium实现都基于特定体系结构的。寄存器分配发生在这个级别。最后,Lithium 被编译成机器码。
五、
最后,这里有一些关于如何编写优化的、更好的JavaScript的技巧。你可以很容易地从上面的内容中推导出这些内容,但是,这里有一个简单的总结:
1、对象属性的顺序:总是以相同的顺序实例化您的对象属性,这样隐藏的类,以及随后优化的代码,都可以被共享。
2、动态属性:在实例化之后将属性添加到对象中,将强制隐藏类更改,并减慢对前一个隐藏类进行优化的任何方法。相反,在构造函数中分配所有对象的属性。
3、方法:重复执行相同方法的代码将比执行许多不同方法的代码运行得更快(由于内联缓存)。
4、数组:避免稀疏数组,其中键不是递增的数字。数组中没有任何元素的稀疏数组是一个哈希表。此类数组中的元素访问成本更高。另外,尽量避免预先分配大型数组。
5、标记值:V8 在表示对象和数字时使用的32位。它会用一位来表示是Object (flag = 1) 还是integar (flag = 0) 。余下还有31位,如果一个数值大于31位,V8将创建一个新的对象来将这个数字放入里面,并把该数字变成一个double。所以尽可能使用31位以内数字,以避免昂贵开销产生。
PS:
https://docs.google.com/document/u/1/d/1hOaE7vbwdLLXWj3C8hTnnkpE0qSa2P–dtDvwXXEeD0/pub
https://github.com/thlorenz/v8-perf
http://code.google.com/p/v8/wiki/UsingGit
http://mrale.ph/v8/resources.html
https://www.youtube.com/watch?v=UJPdhx5zTaw
https://www.youtube.com/watch?v=hWhMKalEicY
想要打赏,请点击这里