JavaScript引擎基本原理:Shapes和Inline Caches
这篇文章描述了一些在js引擎中通用的关键点, 并不只是V8, 这个引擎的作者(Benedikt和Mathias)开发的. 作为一名JavaScript的开发者, 需要较深的理解JavaScript引擎是如何工作的, 那可以帮助你更改的冲原理层面提高你代码的性能.
JavaScript 引擎管道
这是你写出所有JS代码的开始. JS引擎会格式化你的代码, 并他们转成抽象语法树(AST).基于这个AST,解析器能够开始做他的事情, 开始产生字节码. 完美, 就在那一刻, 引擎开始真正的运行JS代码.
为了能让他跑的更快,这些字节码能够和压缩后的数据一起发送给优化编译器, 这些优化编译器能够根据基于压缩后的代码, 做出某些确认的假设. 然后产生优化程度更高的代码.
如果某些假设是不正确的, 那么优化编译器会自动去优化, 并返回解释器. (TODO: 不太理解退回到解释器, 是退回成初识的代码吗?)
JavaScript引擎中的解释器和编译器管道
现在让我们放到到在管道中的一个部分, 那是你真正运行JavaScript的地方. 就是代码被解析和优化的地方. 然后去对比一些在流行的JavaSript引擎中某些不同的地方.
总的来说, 一个管道包含一个解析器和一个优化编译器 . 解释器会快速并源源不断的产生没有被优化过的字节码. 然后优化编译器会多花点时间. 但是最后产生一些优化程度更高的机器码.
这种常见的管道, 几乎和V8中存在的一样. JavaScript引擎在Chrome和Node中是使用方式如下:
解释器在V8中被称为启动装置(Ignition), 负责生成和执行字节码. 当运行这些字节码的时候, 他会收集分析数据, 这些数据用来加速后面的执行. 当一个函数hot的时候. 举个例子, 就是他经常执行的时候, 生成字节码和分析的数据会被通过到TurboFan, 我们的优化编译器, 基于分析的数据会生成更高优化程度的机器码.
SpiderMonkey, Mozilla的JavaScript引擎被用在火狐和SpriderNode上面, 他有一点点的不同. 他有两个优化编译器. 解释器首先使用基础的优化器优化, 产生一些优化后的代码. 当代码开始运行的时候, 会产生一些分析的数据, IonMonkey能够基于这些分析的数据产生更高程度的优化代码, 如果推测的优化项是错误的, 那么IconMonkey就会退回到基础优化器产生的代码.
Chakra, 被用在Edge和Node-ChakraCore中的JavaScript引擎,设置了两个非常小的优化编译器. 解析器使用SimpleJIT开始优化, (JIT表示Just-In-Time compiler实时编译器), 哪里会产生一个优化后的代码. 产生一些分析后的数据, 这个FullJIT能够产生优化程度更高的代码.
JavaScriptCore(简称JSC), 苹果用在Sarari上的和React Native上的JavaScript引擎. 使用三种不同的优化引擎, 使他变得极致. LLInt(Low-Level Interpreter), 最底层的的解析器, 使用基层优化器优化, 然后使用DFG(Data Flow Graph)优化器, 然后再使用FTL(Faster Than Light)优化器.
为什么一些引擎比其他的引擎使用的更多的优化编译器. 这是权衡利弊的结果. 一个解析器能够分成快速的产生字节码, 但是字节码通常不够高效. 另一个方面来说, 一个优化编译器需要花费更长的事件, 但是最终可以产生一些更加高效的机器码. 这就是在更加快速的运行代码或者牺牲一些时间, 最后运行一些性能更高的代码. 一些引擎选择增加多个使用不同时间/高性能的优化编译器, 允许他们对于权衡利弊这事进行更高程度的控制, 但是增加了复杂性. 另一个方面, 权衡利弊也和内存的使用有关系. 后面的文章会有介绍.
我们只是重点讲了在每一个浏览器中, 关于管道中的解析器和优化器的不同. 但基于这些不同, 在更高的层面上, 所有的JavaScript引擎都有相同的特性: 那就是格式化和一些在管道中解析器和优化器的特性.
JavaScript的对象模型
让我们通过放到一些方面的实现来看看JavaScript引擎相同的部分.
举个例子: JavaScript引擎如何实现的JavaScript的对象模型? 又使用来的哪些技巧来提升访问JavaScript对象的性能. 事实证明: 所有主要引擎的实现都非常相似.
ECMAScript规范在本质上定义了所有的对象都作为一个字典, 用一些
key, 去对应一些属性的描述.
除了表示本身的[[value]]
, 这个规范定义了一些其他的属性.
[[writable]]
确定这个属性是否可以重新分配,[[Enumerable]]
确定了这个属性能否在for-in
循环中展示,和
[[Configurable]]
确定了这个属性能否被删除.这两个中括号(double square brackets)的表示, 看起来非常有趣, 这是规范表示不能直接保留的JavaScript属性. 通过使用JavaScript中的
Object.getOwnPropertyDesriptor
API, 你仍然可以任何给定的对象上面属性的描述.const obj = {a:1}
Object.getOwnPropertyDescriptor(obj, 'a')
// {value: 1, writable: true, enumerable: true, configurable: true}
好了, JavaScript就是这么定义对象的. 但是对于数组又是如何定义的呢?
你一定能够想到, 数组作为一个特殊的类型的对象. 其中一个区别就是数组对于数组的索引, 有特殊的处理. ESMA规范规定 数组 索引 是一个特殊的术语. 在JS中数组的最大限制为2^23-1个元素. 数组的索引是任何在限制内的有效值, 就是从0到2^23-2的任何整数.
另一个不同就是数组会有一个特殊的
length
属性.const array = ['a', 'b']
array.length // 2
array[2] = 'c'
array.length // 3
在这个例子中, 数组被创建的时候
length
是2
. 当我们分配另一个元素到索引2的位置上的时候,length
属性自动被修改了.JavaScript定义数组的方式和对象类似. 例如: 所有的属性, 包括数组的索引, 都使用明确的使用字符串表示. 在数组中的第一个元素, 就是存储在属性
'0'
下面.'length'
属性这是一个不能枚举和删除的属性.当一个元素被添加到数组中的时候, JavaScript会自动的更新
length
属性的[[value]]
属性.通常来说, 数组和对象非常相似.
优化属性访问
现在我们知道在JavaScript中对象是如何定义的, 让我们深入到JavaScript引擎如何高效的使用对象工作.
让我们看下最简单的JavaScript程序, 访问属性是最常见的简单操作. 所以, JavaScript引擎能否快爽的属性是至关重要的.
const object = {
foo: 'bar',
baz: 'qux
}
// 在这, 我们访问object上的foo属性
doSomething(objet.foo)
// ^^^^^^^^^
Shapes
在JavaScript程序中, 经常出现多个对象具有相同的属性. 这就表明, 很多对象具有相同的 模型(shape).
const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };
// 此时object1和object2就具有相同的模型
一种非常常见的操作就是, 方位相同模型上的对象上的相同属性
function logX(object) {
console.log(object.x);
// ^^^^^^^^
}
const object1 = { x: 1, y: 2 };
const object2 = { x: 3, y: 4 };
logX(object1);
logX(object2)
这一点非常重要, JavaScript能够基于对象的模型, 优化对象的属性访问. 下面是他的工作方式.
我们假设我们有一个对象, 上面有x
和y
两个属性, 并且他使用的了我们之前讨论的字典数据结构: 使用字符串作为key, 这些key各自指向了他们对于对于属性的描述.
当我们访问其中一个属性, 例如object.y
, 然后引擎会去寻找在JSObject
上的keyy
, 然后找到在一致的属性描述, 最后返回[[Value]]
但是, 这些属性的藐视存储在缓存中的什么位置呢? 我们又是如何将这些描述作为JSObject
一个部分进行存储的呢? 假设我们会看到后面更多的对象,使用这个模型. 这个时候,存储这个对象的整个字典,包括属性名称和描述,就变成了一种浪费. 因为所有具有相同模型的对象都是重复的. 这会造成非常多重复和不必要的内存使用. 作为一种优化方式, 引擎会存储个别对象的模型Shape
.
模型包含了除了[[Value]]
以外所有的属性名称和描述. 相反Shape
包含了JSObject
内部值的偏移量(offset), 所以引擎可以获取到正确的values值. 每一个JSObject
都有都用这个相同的模型指针表示精确的模型接口. 现在每一个JSObject
只需要存储属于他的独一无二的值.
好处就是当我们有多个对象的时候, 好处就变得明显. 无论我们有多少个对象, 只要他们有相同的墨香, 我们只需要存储一此模型和属性的信息.
所有的JavaScript引擎都用到了模型的优化手段, 但是他们不一定成为模型:
Academic papers: Hidden Classes (让人和js中的class搞混)
V8: Maps (容易和
Map
混淆)Chakra: Types (容易和js中的动态类型, 还有
typeof
运算符搞混)JavaScriptCore: Structures
SpiderMonkey: Shapes
在本文中, 我们将继续使用shapes这个单词.
Transition chains and tress
当你有一个对象, 这个对象有一个确定的模型, 但是当你添加一个属性的时候, 会发生什么? 引擎发现一个新的模型的时候, 如何如何?
const object = {};
object.x = 5;
object.y = 6
这种模型在JavaScript引擎中被称为 转换链(transition chains). 举个例子:
这个对象起初时没有任何属性, 所以他指向一个空的模型. 当下一个操作添加了一个属性x
, 并且赋值为5
, 这时引擎就会转换为包含一个属性x为5, 并且第一个的偏移量为0
的模型. 当再次添加属性y的时候, 引擎再次转换为另一个包含x和y的模型, 并且y的偏移量为1对应到JSObject
上.
提示: 模型收到添加属性顺序的影响. 例如:
{ x: 4, y: 5}
和{ y: 5, x: 4 }
使用的是不同的模型
我们甚至不需要存储每一个模型的完整属性表. 相反, 每一个模型只需要知道新的属性的信息. 举个例子: 在下面的案例中, 我们并没有在最后一个模型中存储属性x的信息. 因为可以在之前的链(chain)找到他. 为了实现这个功能, 每一个模型, 都链接了他的上一个模型.
如果你在代码中写下o.x
, 引擎就会沿着转换链一层层的向上寻找, 一直找到一个模型有关于属性'x'的信息.
但当我们不能创建一个原型链的时候呢?举个例子:首先我们有两个空对象, 然后我们对于他们分别添加不同的属性.
const object1 = {};
object1.x = 5;
const object2 = {};
object2.y = 6;
在这个例子中, 我们有一个分支来代替链, 结束的时候, 我们有一个 转换树(transition tree)
在这, 我们创建了一个空对象a
, 然后给他添加了属性x
. 结束后, JSObject
包含一个唯一值和两个模型: 一个空模型, 和一个只包含属性x的模型.
第二个例子开始的时候, 我们有一个控模型b
, 然后添加了一个属性y
. 最后, 我们有了两个模型连, 一共是有三个模型.
那是不是意味着我们总是在开始时使用一个空模型?未必,引擎会针对那些含有确定属性的对象进行优化. 让我们给另外一个空对象添加属性x, 然后有一个对象已经拥有了确定的属性x
const object1 = {};
object1.x = 5;
const object2 = { x: 6 };
在这个例子中, 首先我们是有一个空模型, 然后转换到一个包含属性x
的模型. 就像我们之前看到的那样.
在object2
的例子中, 直接产生包含x属性的一个对象是意义的, 而不是先产生空对象, 再去转换.
这个对象的描述, 一开始就从一个, 包含一个属性x
的模型开始的. 有效的跳过了空模型. 这是V8和SpiderMonkey所使用的. 这种优化模式缩短了转换链, 并让对象的构造更加高效.
Benedikt's 的博客surprising polymorphism in React applications讨论了这些微小的细节如何影响所展示的真实性能.
下面是一个拥有'x', 'y', 和'z'的属性的3D的例子.
const point = {};
point.x = 4;
point.y = 5;
point.z = 6;
通过我们前面学到的, 会用在内存中使用三个模型来创建这个对象(并没有计算空模型). 当访问属性x
的时候, 比如, 你在程序里写到point.x
在你的程序里. 引擎需要沿着转换链线性寻找. 他会先寻找最下面的模型. 然后一层层向上寻找, 一直在最上面的模型中找到属性x
的描述.
当我们做的操作越来越多的时候, 那一定会变得非常慢. 尤其是当一个对象具有非常多的属性的时候. 寻找属性的时间复杂度为O(n)
, 即和对象上的属性数量线性相关. 为了加快搜索属性, JavaScript引擎加入了一个ShapeTable
的数据结构. 这个ShapeTable
是一个字典, 其中属性key映射不同模型上的属性描述.
稍等, 现在让我们往前想一想... 这就是我们之前添加模型的地方. 这就是关于模型的全部.
模型的处理方式是非常有效的. 另一种优化方式称之为 内嵌缓存(Inline Caches)
Inline Caches(ICs)
模型背后的主要动机是内嵌缓存(Inline Caches/ ICs)的概念. ICs是JavasCript快速运行的重要因素. 引擎使用ICs来记录找到对象属性的地方, 减少昂贵的查找次数.
这是函数getX
, 他接受一个对象, 并加载属性x
function getX(o) {
return o.x;
}
如果我们在JSC中运行这个环节, 他会产出下面字节码.
首先, get_by_id
从第一个参数中加载属性x, 并将结果存储到loc0
中. 第二条命令然后我们存储在loc0
中存储的结果.
JAC也嵌入了内嵌缓存到get_by_id
指令中, 有两个未初始化的插槽构成.
现在, 我们假设传入一个对象{ x: 'a' }
, 来执行getX
这个函数. 前面学到的, 这个对象有一个模型, 这个模型上有属性x
, 然后这个Shape
存储了偏移量, 和关于属性x的描述. 当你在第一时间执行这个函数的时候, get_by_id
指令会去向上查找属性x
, 然后发现值是存储在偏移量为0
的位置.
这个内嵌了的IC, 进入到get_by_id
指令中, 缓存了模型, 和需要寻找属性的偏移量.
后面这个函数再次执行的时候, IC只需要对比模型, 发现和上一个模型一样, 那么只需要加载从存取的偏移量取值. 明确一点, 如果引擎发现IC之前记录了这个对象使用的模型, 那么他不再需要去查询出这个属性的全部信息. 相反的, 能够直接跳过昂贵的属性信息查找. 这对于每一次都要查找属性的速度提升是显而易见的.
高效的数组存储
对于数组来说, 经常遇到把数组的索引作为属性存储起来. 每一个属性对应的数值, 称之为数组元素. 把每一个相同的数组中的元素的信息都存储起来, 是非常浪费空间的. 相反的, 引擎利用数组元素的属性是可修改, 可枚举, 可删除这个一个默认配置, 将数组元素和其他的命名元素分成存储.
思考下面的数组:
const array = [
'#jsconfeu'
];
引擎存储了数组的长读(1
), 并且指向了Shape
, 这里包含了偏移量, 和关于属性length
的属性描述.
这和我们之前看到的非常相似, 那么数组的值存储在哪里呢?
每一个数组都有一个单独的 元素单元(element backing) 进行存储包含在索引对应的所有的属性的值. JavaScript引擎不会存储任何元素的属性描述, 因为他们总是可编辑, 可枚举, 可删除的.
可是, 在不正常的例子下会发生什么呢? 如果你感觉数组元素的属性描述呢?
// please don't ever do this
const array = Object.defineProperty(
[],
'0',
{
value: 'Oh noes!!1',
writable: false,
enumerable: false,
configurable: false,
}
);
上面的代码中定义了一个属性0
, (但是这又刚好是数组的索引), 但设置他的属性描述为非默认值.. (说真的, 没看懂...)
在这种极限情况下, 引擎支持使用一个字典来映射整个数组元素的存储空间.
即使我们只有一个数组元素使用了非默认配置的属性, 那整个数组的存储空间都会变慢, 变成一种毫无效果的模式. 避免Object.defineProperty
在数组上的使用 (我不知道你为什么要这么做, 他看起来毫无用处.)
另外几点
我们学习了引擎如何存储对象和数组, 已经Shapes和ICs如果优化那些常见的操作. 基于这些只是, 我们可以总结出来几点在实际写代码的时候, 能够帮助促进性能的建议:
- 经常使用同一种方式初始化你的对象, 这样他们在结束的时候, 就不会产生不同的模型
- 不要搞错数组元素的属性描述, 让他们更加高效的存储和操作
Note: 这是我的第一篇原文翻译文章
JavaScript引擎基本原理:Shapes和Inline Caches的更多相关文章
- JavaScript 引擎基础:Shapes 和 Inline Caches
JavaScript 引擎基础:Shapes 和 Inline Caches hijiangtao 中国科学院大学 计算机应用技术硕士 260 人赞同了该文章 前言:本文也可以被称做 “JavaS ...
- JS 引擎基础之 Shapes and Inline Caches
阅读下面这篇文章,需要20分钟: 一起了解下 JS 引擎是如何运作的吧! JS 的运作机制可以分为 AST 分析.引擎执行两个步骤: JS 源码通过 parser(分析器)转化为 AST(抽象语法树) ...
- JavaScript引擎基本原理: 优化prototypes
原文链接: JavaScript engine fundamentals: optimizing prototypes 这篇文章介绍了一些JavaScript引擎常用的优化关键点, 并不只是Bened ...
- 对JavaScript 引擎基础:Shapes 和 Inline Caches
全文有5个部分组成 1.JavaScript 引擎工作流程:介绍 JavaScript 引擎的处理流水线,这一部分会涉及到解释器/编译器的内容,且会分点介绍不同引擎间的差别与共同点: 2.JavaSc ...
- 对JavaScript 引擎基础:原型优化的研究 -----------------------引用
一.优化层级与执行效率的取舍 介绍了现代 JavaScript 引擎通用的工作流程: 我们也指出,尽管从高级抽象层面来看,引擎之间的处理流程都很相似,但他们在优化流程上通常都存在差异.为什么呢?为什么 ...
- V8 Javascript 引擎设计理念
Netscape Navigator 在 90 在年代中期对 JavaScript 进行了集成,这让网页开发人员对 HTML 页面中诸如 form .frame 和 image 之类的元素的访问变得非 ...
- V8 javascript 引擎
V8是一个由丹麦Google开发的开源java script引擎,用于Google Chrome中.[2]Lars Bak是这个项目的组长.[3] V8在执行之前将java script编译成了机 ...
- v8引擎详解(摘)-- V8引擎是一个JavaScript引擎实现
随着Web相关技术的发展,JavaScript所要承担的工作也越来越多,早就超越了“表单验证”的范畴,这就更需要快速的解析和执行JavaScript脚本.V8引擎就是为解决这一问题而生,在node中也 ...
- V8:V8(Javascript引擎)
ylbtech-V8:V8(Javascript引擎) Lars Bak是这个项目的组长,目前该JavaScript引擎已用于其它项目的开发.第一个版本随着第一个版本的Chrome于2008年9月2日 ...
随机推荐
- Python序列——Unicode
Unicode是什么 Python中的Unicode 编码与解码 在应用中使用Unicode的建议 1. Unicode是什么 Unicode是对字符进行编码的一种标准.而utf8或者utf-8是根据 ...
- Codeforces Round #261 (Div. 2) B. Pashmak and Flowers 水题
题目链接:http://codeforces.com/problemset/problem/459/B 题意: 给出n支花,每支花都有一个漂亮值.挑选最大和最小漂亮值得两支花,问他们的差值为多少,并且 ...
- hadoop集群异常问题总结
1. Could not find or load main class java.library.path=.opt.hadoop.lib 我的环境上是 hadoopopts变量的配置问题,至于为啥 ...
- SQLite多线程使用总结
SQLite支持3种线程模式: 单线程:这种模式下,没有进行互斥,多线程使用不安全.禁用所有的mutex锁,并发使用时会出错.当SQLite编译时加了SQLITE_THREADSAFE=0参数,或者在 ...
- 使用boost库生成 随机数 随机字符串
#include <iostream> #include <boost/random/random_device.hpp> #include "boost/rando ...
- GetModuleFileNameW
GetModuleFileNameW( HMODULE hModule, //模块句柄 或应用程序的实例句柄 若参数为NULL,则返回该应用程序全路径 __out_ecount(nSize) LPWS ...
- Resistance
题意: 给出一个由n个节点和m个二元电阻元件组成的电路,求问节点1到节点n的等效电阻. 解法: 应用电子电路分析中的基尔霍夫定律,对于每一个点有流量平衡,得 对于点$x$有 $$I_{出} + \su ...
- g2o使用bug总结
g2o进行3d2d优化的时候,设置优化图的边时,注意setVertex()中顶点的顺序. void setVertex(size_t i, Vertex* v) { assert(i < _ve ...
- 1.5-1.6 oozie部署
一.部署 可参考文档:http://archive.cloudera.com/cdh5/cdh/5/oozie-4.0.0-cdh5.3.6/DG_QuickStart.html 1.解压oozie ...
- java集合框架之Collection
参考http://how2j.cn/k/collection/collection-collection/366.html Collection是 Set List Queue和 Deque的接口Qu ...