【JSConf EU 2018】JavaScript引擎: 精粹部分

简介: JSConf EU 2018圆满结束, 谷歌V8的开发者Mathias Bynens以及Benedikt Meurer一起发表了《JavaScript Engines: The Good Parts™》演讲,本文将带领大家回顾一下演讲上所提到的重点。

JSConf EU 2018圆满结束, 谷歌V8的开发者Mathias Bynens以及Benedikt Meurer一起发表了《JavaScript Engines: The Good Parts™》演讲,本文将带领大家回顾一下演讲上所提到的重点。

演讲第一部分: JavaScript引擎

JavaScript引擎

JavaScript引擎解析源代码并将其转换成抽象语法树(AST)。基于AST,解释器产生字节码。此时,引擎正在运行JavaScript代码。为了加快运行速度,字节码连同分析数据一起发送到编译器。编译器根据已有的分析数据做出某些假设,然后生成优化后机器代码。


9a017da73d3245ca1d20db6c3e380f02bcfe7dc6

JavaScript引擎中的解释器/编译器

通过对比主流JavaScript引擎之间的一些实现差异来说明JavaScript引擎是如何运行你的代码。

解释器快速生成未优化的字节码,编译器会花费更长的时间,但最终产生高度优化的机器代码。

a4bb94388c74b2ff818234f6ce503885fa951a85

以上基本就是V8在Chrome和Node.js中的工作流程

b380f02826b23011dd018ab9225242b82a7cf84a

V8的解释器负责生成和执行字节码。当它运行字节码时,它收集分析数据,这些数据是优化的依据。当函数运行时,生成的字节码和分析数据被传递给TurboFan编译器,基于分析数据生成高度优化的机器代码。

d4b42f75b7732b95db45f56de8dfd8f0dca58bda

SpiderMonkey是Mozilla的JavaScript引擎,在Firefox和SpiderNode中使用,它和我们上面所讲的流程有点不同。它有两个编译器。Baseline编译器生成一些优化的代码。结合在运行代码时收集的分析数据,IonMonkey编译器可以产生重度优化的代码。如果优化失败,IonMonkey 回退到Baseline的优化代码。

5d4d0bb42d9fc392b13799a53ebde91575b7ce39
Chakra,微软的JavaScript引擎,用于Edge和Node-ChakraCore,有非常类似的两个优化编译器。解释器生成的字节码先通过SimuleJIT生成优化代码,这里的JIT代表即时编译器。结合分析数据,FuljJIT可以产生更加的优化代码。

70354a883392dfb8732f21d538f602145b7029fd
JavaScriptCore(简称 JSC),苹果的JavaScript引擎,用于Safari和React Native,它包含三种不同的编译器。LLInt解释器生成字节码,可以经过Baseline编译器生成优化的代码。还可以通过DFG编译器进行进一步优化,最后还可以交给FTL编译器进行优化。

解释器可以快速生成字节码,但字节码通常执行效率不高。另一方面,编译器需要更长的时间,但最终会产生更高效的机器代码。快速获取代码以运行(解释器)或占用更多时间,但最终以最佳性能运行代码(编译器)之间存在权衡。

演讲第二部分:JavaScript的对象模型

ECMAScript规范基本上将所有对象定义为字典,并将字符串键映射到描述对象。


cd0c44eb09d02c3fef714f2d8d5cfc0f3e5529af
JavaScript对于数组的定义类似于对象。例如,包括数组索引在内的所有键都显式表示为字符串。数组中的第一个元素存储在键“0”。
b6fcbaf884c21a0d99bc97dcb60bc540732521e5
“长度”属性只是另一个不可枚举和不可配置的属性。一旦元素添加到数组中,JavaScript会自动更新“length”属性的[[Value]描述对象。
9853f8017ca04a0f789bbe715ef949b70cadb659

演讲第三部分:属性的访问优化

属性访问是JavaScript程序中最常见的操作。对JavaScript引擎来说,快速访问属性是至关重要的。


const object = {
	foo: 'bar',
	baz: 'qux',
};

// Here, we’re accessing the property `foo` on `object`:
doSomething(object.foo);
//          ^^^^^^^^^^

Shape

在JavaScript程序中,具有相同属性键的对象是常见的。这样的对象具有相同的Shape。


const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };
// `object1` and `object2` have the same shape.`
在相同Shape的对象上访问相同的属性也是非常常见的:
`function logX(object) {
	console.log(object.x);
	//          ^^^^^^^^
}

const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };

logX(object1);
logX(object2);

所以,JavaScript引擎可以基于对象的Shape优化属性的访问。

假设我们有一个属性为x和y的对象,它使用我们前面讨论过的字典数据结构:它包含作为字符串的键,并且他们指向各自属性的描述对象。

3dcec8e4bf3ed9da050e50098d56397ef6659a36

如果你访问了一个属性,例如object.y,JavaScript引擎将在js对象中查找关键字“y”,然后加载相应的描述对象,最后返回[[Value]]属性的值。

如果每个JS对象都存储描述对象,会造成大量的重复和不必要的内存开销。JavaScript引擎会将这些对象的Shape分开存储。


8b91aa0f4107a4d7b081231f00f212823e1864d1
这个Shape使用offset代替了[[Value]],每一个具有相同Shape的JS对象都指向这个Shape实例。
0628f3a2d4d789ff006c1223bf8f4800a7eb272a
当有多个对象时,只要它们有相同的Shape,只需要存储一个就可以!

所有JavaScript引擎都使用Shape作为优化,但它们并不都称之为Shape:

  • 学术论文称之为Hidden Classes
  • V8称之为Maps
  • Chakra称之为Types
  • JavaScriptCore称之为Structures
  • SpiderMonkey称之为Shapes 演讲中统一使用了Shape。

过渡链与过渡树

如果一个对象指向某个Shape,你给它添加一个新的属性,JavaScript引擎如何找到新的Shape。这类Shape在JavaScript引擎中形成所谓的“过渡链”。下面是一个例子:


4faa9997113bfaee6ab5b8613197aa188e73faa0
对象开始时没有任何属性,因此指向空Shape。下一个语句将一个值为5键为“x”的属性赋值给这个对象,因此JavaScript引擎将JS对象指向一个包含属性“x”的Shape,并且将5添加到JS对象的第0位。下一行代码添加了一个属性“y”,因此引擎将JS对象指向另一个包含属性“x”和属性“y”的Shape,并且将6追加到JS对象的第1位。

我们甚至不需要为每个Shape存储完整的属性表。相反,每一个Shape仅需要知道它所引入的新属性。例如,在这种情况下,我们不必在最后一个Shape中存储关于“x”的信息,因为它可以在链中更早地找到。为了做到这一点,每一个Shape都和上一个Shape产生链接:


f8186fd7b4460ff8ef33009ae940a34de6901cee
如果你在JavaScript代码中编写了o.x,JavaScript引擎通过过渡链找到引入属性“y”的Shape,从而找到找到属性“x”。

但是如果没有办法创建一个过渡链怎么办?例如,如果有两个空对象,并且向每个对象添加不同的属性呢?


const object1 = {};
object1.x = 5;
const object2 = {};
object2.y = 6;

在这种情况下,我们必须使用分支取代链,我们最终得到一个过渡树:

4546fc9f2f00aad0704356524c25caf51be38655

引擎对已经包含属性的对象应用了一些优化。要么从空对象开始添加“x”,要么有一个已经包含“x”的对象:


const object1 = {};
object1.x = 5;
const object2 = { x: 6 };

52b17dc0590e08486cb4c574a0d10a6bbd4a0078
对象在一开始就指向包含属性“x”的Shape,有效地跳过空Shape。V8和SpiderMonkey就是这样做的。这种优化缩短了过渡链,并使其更高效地从文字构造对象。

内联缓存(ICs)

ICs是使JavaScript快速运行的关键因素!JavaScript引擎使用ICs来记住在何处查找对象属性的信息,以减少查找次数。 这里有一个函数getX,它获取一个对象并从中加载属性“x”:



function getX(o) {
	return o.x;
}

如果我们在JSC中运行这个函数,它会生成下面的字节码:

89f5cc425db199eba37cf2c06d3bcf64a237f8d4
第一个get_by_id指令从第一个参数(arg1)加载属性“x”,并将结果存储到loc0中。第二个指令返回我们存储到的LoC0。

JSC还将内联缓存嵌入到get_by_id指令中,该指令由两个未初始化的槽组成。

0a2f8d9cebc420df4a372d10cff51be1bd1ad615

现在假设我们使用{x:“a”}参数来调用getX。如我们所知,这个对象指向有属性“x”的Shape,并且该Shape存储了属性“x”的偏移量和描述对象。当第一次执行该函数时,get_by_id指令查找属性“x”,并发现该值被存储在偏移量0。

3827fe56416bf5e4043c0d78cd31e90bce8c594c

嵌入到get_by_id指令中的IC记住了这个属性是从哪个Shape以及偏移量中找到的:

c8da610360d5d83b736c949fd6bf51ae6fefb49d

对于后续的运行,IC只需要比较Shape,如果它与以前相同,只需从存储的偏移量中加载值即可。具体地说,如果JavaScript引擎看到对象指向了IC之前记录的Shape,那么就不需要重新去查找,可以完全跳过昂贵的属性查找。这比每次查找属性要快得多。

演讲第四部分:有效的存储数组

数组使用数组索引来存储属性。这些属性的值称为数组元素。为每个数组元素存储描述对象是不明智的。数组索引属性默认为可写、可枚举和可配置,JavaScript引擎将数组元素与其他属性分开存储。

看一下这个数组:


const array = [
	'#jsconfeu',
];

引擎存储的数组长度为1,并指向包含length的Shape,偏移值为0。
755d827235816e948401197c455084cea0a4a2c4
9d523492859a1a887532a9cf3e8a3e4f4429b0f4
每个数组都有一个单独的元素后备存储区,它包含所有数组索引的属性值。JavaScript引擎不必为每个数组元素存储任何描述对象,因为它们通常都是可写的、可枚举的和可配置的。

如果更改数组元素的描述对象,会怎么样?


// Please don’t ever do this!
const array = Object.defineProperty(
	[],
	'0',
	{
		value: 'Oh noes!!1',
		writable: false,
		enumerable: false,
		configurable: false,
	}
);

上面的代码段定义了一个名为“0”的属性(恰好是一个数组索引),但它将属性设置为非默认值。

在这样的极端情况下,JavaScript引擎将整个元素后备存储区作为字典,映射描述对象到每个数组索引。

b7a8a487b72531f4679ce33569b77e23dcbca5ae

即使只有一个数组元素有非默认描述对象,整个数组的元素后备存储区也会进入这个缓慢而低效的模式。避免在元素索引上使用Object.defineProperty!

结语

本次演讲让我们明白JavaScript引擎是如何工作的,如何存储对象和数组,以及如何通过Shape和ICs优化了属性的访问,如何优化了数组的存储。基于这些知识,为我们确定了一些实用的可以帮助提高性能的编码技巧:

  • 总是以同样的方式初始化对象,它们最终会有相同的Shape。
  • 不要修改数组元素的描述对象,它们可以有效地存储。

注记

  • 本文结构及代码来自 Mathias Bynens以及Benedikt Meurer 在 JSConf EU 2018 上所作的演讲 JavaScript Engines: The Good Parts™。录像地址:https://www.youtube.com/watch?v=5nmpokoRaZI&index=11&list=PL37ZVnwpeshG2YXJkun_lyNTtM-Qb3MKa
  • 同时也可以阅读本次演讲的Blog:https://mathiasbynens.be/notes/shapes-ics


原文发布时间为:2018年06月19日
原文作者:想成为工匠的码农
本文来源: 掘金  如需转载请联系原作者
相关文章
|
1月前
|
Web App开发 JavaScript 前端开发
Node.js 是一种基于 Chrome V8 引擎的后端开发技术,以其高效、灵活著称。本文将介绍 Node.js 的基础概念
Node.js 是一种基于 Chrome V8 引擎的后端开发技术,以其高效、灵活著称。本文将介绍 Node.js 的基础概念,包括事件驱动、单线程模型和模块系统;探讨其安装配置、核心模块使用、实战应用如搭建 Web 服务器、文件操作及实时通信;分析项目结构与开发流程,讨论其优势与挑战,并通过案例展示 Node.js 在实际项目中的应用,旨在帮助开发者更好地掌握这一强大工具。
46 1
|
2月前
|
JavaScript 前端开发 Java
JS引擎V8
【10月更文挑战第9天】
35 0
|
4月前
|
Web App开发 JavaScript 前端开发
什么是JavaScript引擎
【8月更文挑战第14天】什么是JavaScript引擎
106 1
|
6月前
|
XML 缓存 JavaScript
一篇文章讲明白JS模板引擎之JST模板
一篇文章讲明白JS模板引擎之JST模板
61 2
|
7月前
|
JavaScript 前端开发 NoSQL
【MongoDB 专栏】MongoDB 的 JavaScript 引擎与脚本执行
【5月更文挑战第11天】MongoDB 的 JavaScript 引擎允许在服务器端直接执行脚本,提升效率并实现定制化操作。脚本环境提供独立但与数据库关联的运行空间,引擎负责脚本的解析、编译和执行。执行过程包括脚本提交、解析、编译和执行四个步骤。掌握脚本逻辑设计和 JavaScript 语言特性对于高效利用这一功能至关重要。例如,通过脚本可以计算商品总销售额,增强数据库操作的灵活性。
124 1
【MongoDB 专栏】MongoDB 的 JavaScript 引擎与脚本执行
|
6月前
|
缓存 自然语言处理 前端开发
深入剖析JavaScript引擎的工作原理
【6月更文挑战第3天】JavaScript引擎由解析器、解释器、优化器和垃圾回收器组成,它们协同完成代码的解析、编译和执行。解析器将源代码转为抽象语法树(AST),编译阶段进行作用域分析和变量提升。解释器执行AST,优化器在代码频繁执行时进行即时编译以提高性能。垃圾回收器自动回收不再使用的内存,防止泄漏。理解这些原理有助于优化代码和提升Web应用性能。
59 1
|
7月前
|
JavaScript 前端开发 Go
8 大博客引擎 jekyll/hugo/Hexo/Pelican/Gatsby/VuePress/Nuxt.js/Middleman 对比
探索各类博客引擎:Jekyll、Hugo、Hexo、Pelican、Gatsby、VuePress、Nuxt.js和Middleman的对比,包括语言、模板引擎、速度、社区活跃度等。了解每种引擎的优缺点,助你选择合适的博客构建工具。查看详细文章以获取更多实战和安装指南。
|
7月前
|
JavaScript 前端开发 开发者
Vue.js深度解析:前端开发的生产力引擎
Vue.js深度解析:前端开发的生产力引擎
115 0
|
JavaScript 前端开发 算法
带你读《现代Javascript高级教程》十一、JavaScript引擎的垃圾回收机制(1)
带你读《现代Javascript高级教程》十一、JavaScript引擎的垃圾回收机制(1)
|
JavaScript 前端开发 算法
带你读《现代Javascript高级教程》十一、JavaScript引擎的垃圾回收机制(2)
带你读《现代Javascript高级教程》十一、JavaScript引擎的垃圾回收机制(2)