V8引擎是如何工作?

56次阅读

共计 3435 个字符,预计需要花费 9 分钟才能阅读完成。

V8 是 google 开发的 JavaScript 引擎, 它是开源的,而且是用 C ++ 编写的。它是用于客户端(Google Chrome)和服务器端(node.js)JavaScript 应用程序。
V8 最初旨在提高 Web 浏览器中 JavaScript 执行的性能。为了提升速度,V8 将 JavaScript 代码转换为更高效的机器语言,而不是使用解释器。它通过实现 JIT(即时编译器)将 JavaScript 代码编译成机器代码,就像许多现代 JavaScript 引擎(如 SpiderMonkey 或 Rhino(Mozilla))所做的那样。与 V8 的主要区别在于它不会产生字节码或任何中间代码。
本文的目的是展示和理解 V8 如何工作,以便为客户端或服务器端应用程序生成优化的代码。如果您已经在问自己“我应该关心 JavaScript 性能吗?”那么我将回答 Daniel Clifford(技术主管和 V8 团队经理)的一句话:“这不仅仅是让您当前的应用程序运行得更快,而是关于实现你过去从未做过的事情“。
隐藏的 class
JavaScript 是一种基于原型的语言:no classes,并且使用克隆过程创建对象(原型链)。JavaScript 也是动态类型的:类型和类型信息不是显式的,属性可以动态添加到对象中或从中删除。有效访问类型和属性是 V8 的首要挑战。而不是使用类似字典的数据结构来存储对象属性和进行动态查找来解析属性位置(就像大多数 JavaScript 引擎一样),V8 在运行时创建隐藏类,以便具有内部表示类型系统和改善属性访问时间。
让我们有一个 Point 函数和两个 Point 对象的创建:
https://p1.ssl.qhimg.com/t016…
如果布局相同(这里是这种情况),则 p 和 q 属于由 V8 创建的相同隐藏类。这突出了使用隐藏类的另一个优点:它允许 V8 对属性相同的对象进行分组。这里 p 和 q 有一定的代码优化。
现在,让我们假设我们想在我们的 q 对象之后添加一个 z 属性,就在它声明之后(对于动态类型语言来说这是完全没问题的)。
V8 将如何处理这种情况?事实上,每当构造函数声明一个属性并跟踪隐藏类的变化时,V8 就会创建一个新的隐藏类。为什么?因为如果创建了两个对象(p 和 q)并且在创建后将成员添加到第二个对象(q),则 V8 需要保留最后创建的隐藏类(对于第一个对象 p)并创建一个新对象(对于第二个对象 q)与新成员。
https://p4.ssl.qhimg.com/t01c…
每次创建一个新的隐藏类时,前一个隐藏类都会更新一个类转换,指示必须使用哪个隐藏类。
因此:

初始化构造函数中的所有对象成员(因此实例稍后不会更改类型)
始终以相同的顺序初始化对象成员

代码优化
因为 V8 为每个属性创建一个新的隐藏类,所以应该将隐藏的类创建保持在最低限度。为此,请尽量避免在创建对象后添加属性,并始终以相同的顺序初始化对象成员(以避免创建不同的隐藏类树)。
[Update] 另一个技巧:单态操作是仅对具有相同隐藏类的对象起作用的操作。当我们调用一个函数时,V8 会创建一个隐藏类。如果我们用不同的参数类型再次调用它,V8 需要创建另一个隐藏类:首选单态代码到多态代码
有关 V8 如何优化 JavaScript 代码的更多示例
标记值
为了有效地表示数字和 JavaScript 对象,V8 表示具有 32 位值。它使用一个位来知道它是一个对象(flag = 1)还是一个整数(flag = 0),这里称为 SMall Integer 或 SMI,因为它的 31 位。然后,如果数值大于 31 位,则 V8 将对该数字进行选择,将其变为双精度并创建一个新对象以将数字放入其中。
代码优化:尽可能使用 31 位带符号数字,以避免对 JavaScript 对象进行消耗性能的封装操作。
数组
V8 使用两种不同的方法来处理数组:

快速元素:专为那些键组非常紧凑的阵列而设计。它们具有线性存储缓冲区,可以非常有效地访问它。
字典元素:专为稀疏数组而设计,它们内部没有所有元素。它实际上是一个哈希表,它的性能消耗比“快速元素”更昂贵。

代码优化:确保 V8 使用“快速元素”来处理数组,换句话说,避免使用稀疏数组。另外,尽量避免预先分配大型数组。最后,不要删除数组中的元素:它使键集稀疏。
a = new Array();
for (var b = 0; b < 10; b++) {
a[0] |= b; // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
a[0] |= b; // Much better! 2x faster.
}
此外,双精度阵列更快 – 数组的隐藏类跟踪元素类型,并且仅包含双精度的数组是未装箱的(这会导致隐藏的类更改)。但是,由于装箱和拆箱,粗心操作阵列会导致额外的工作 – 例如
var a = new Array();
a[0] = 77; // Allocates
a[1] = 88;
a[2] = 0.5; // Allocates, converts
a[3] = true; // Allocates, converts
效率低于:
var a = [77, 88, 0.5, true];
V8 如何编译 JavaScript 代码?
V8 有两个编译器!
一个“完整”编译器,可以为任何 JavaScript 生成良好的代码。此编译器的目标是快速生成代码。为了实现其目标,它不进行任何类型分析,也不了解类型。相反,它使用内联缓存或“IC”策略来在程序运行时优化有关类型的知识。IC 效率非常高,速度可提高 20 倍。
优化编译器,可为大多数 JavaScript 语言生成出色的代码。它稍后会重新编译热门功能。优化编译器从内联缓存中获取类型,并决定如何更好地优化代码。但是,某些语言功能尚不支持,例如 try / catch 块。(try / catch 块的解决方法是在函数中编写“非稳定”代码并在 try 块中调用函数)
代码优化:V8 还支持去优化:优化编译器从内联缓存中对不同类型做出假设,如果这些假设无效则会进行去优化。例如,如果生成的隐藏类不是预期的类,则 V8 会抛弃优化的代码并返回到完整编译器以从内联缓存中再次获取类型。此过程很慢,应该通过在优化后尝试不更改功能来避免。
资源

谷歌 I / O 2012“与 V8 打破 JavaScript 速度限制”,V8 团队的技术主管兼经理 Daniel Clifford:视频和幻灯片。
V8:一个开源 JavaScript 引擎:Lars Bak,V8 核心工程师的视频。
Nikkei Electronics Asia 博客文章:为什么新的谷歌 V8 引擎如此之快?

博客评论由 Disqus 提供
译者注:关于本文中提到的一些知识点,做一些简单的只是扩展,希望对你们理解本文有一些帮助;1、“JavaScript has no classes” 虽然 JavaScript 是面向对象的语言,但它不是基于类的语言 – 它是基于原型的语言。在 js 和 java 或其他“基于类”的编程语言中类的工作方式之间存在一些深刻的差异。相关讨论 2、快速元素和字典元素快速或字典元素:元素的第二个主要区别是它们是快速还是字典模式。快速元素是简单的 VM 内部数组,其中属性索引映射到元素存储中的索引。但是,这种简单的表示对于非常大的稀疏 / 多孔数组而言是相当浪费的,其中只占用很少的条目。在这种情况下,我们使用基于字典的表示来节省内存,但代价是访问速度稍慢:
const sparseArray = [];
sparseArray[9999] = ‘foo’; // Creates an array with dictionary elements.
sparseArray.length
// 10000
sparseArray[0]
// undefined
在这个例子中,分配一个包含 10k 条目的完整数组会相当浪费。相反,V8 会创建一个字典来存储键值描述符三元组。在这种情况下,密钥是 ’9999’,并且使用值 ’foo’ 和默认描述符。鉴于我们没有办法在 HiddenClass 上存储描述符详细信息,只要您使用自定义描述符定义索引属性,V8 就会转向减慢元素:
const array = [];
Object.defineProperty(array, 0, {value: ‘fixed’ configurable: false});
console.log(array[0]); // Prints ‘fixed’.
array[0] = ‘other value’; // Cannot override index 0.
console.log(array[0]); // Still prints ‘fixed’.
引用文档

正文完
 0