原文链接: JavaScript engine fundamentals:Shapes and line Cahes

这篇文章描述了一些在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.getOwnPropertyDesriptorAPI, 你仍然可以任何给定的对象上面属性的描述.

    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

    在这个例子中, 数组被创建的时候length2. 当我们分配另一个元素到索引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能够基于对象的模型, 优化对象的属性访问. 下面是他的工作方式.

我们假设我们有一个对象, 上面有xy两个属性, 并且他使用的了我们之前讨论的字典数据结构: 使用字符串作为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的更多相关文章

  1. JavaScript 引擎基础:Shapes 和 Inline Caches

    JavaScript 引擎基础:Shapes 和 Inline Caches hijiangtao ​ 中国科学院大学 计算机应用技术硕士 260 人赞同了该文章 前言:本文也可以被称做 “JavaS ...

  2. JS 引擎基础之 Shapes and Inline Caches

    阅读下面这篇文章,需要20分钟: 一起了解下 JS 引擎是如何运作的吧! JS 的运作机制可以分为 AST 分析.引擎执行两个步骤: JS 源码通过 parser(分析器)转化为 AST(抽象语法树) ...

  3. JavaScript引擎基本原理: 优化prototypes

    原文链接: JavaScript engine fundamentals: optimizing prototypes 这篇文章介绍了一些JavaScript引擎常用的优化关键点, 并不只是Bened ...

  4. 对JavaScript 引擎基础:Shapes 和 Inline Caches

    全文有5个部分组成 1.JavaScript 引擎工作流程:介绍 JavaScript 引擎的处理流水线,这一部分会涉及到解释器/编译器的内容,且会分点介绍不同引擎间的差别与共同点: 2.JavaSc ...

  5. 对JavaScript 引擎基础:原型优化的研究 -----------------------引用

    一.优化层级与执行效率的取舍 介绍了现代 JavaScript 引擎通用的工作流程: 我们也指出,尽管从高级抽象层面来看,引擎之间的处理流程都很相似,但他们在优化流程上通常都存在差异.为什么呢?为什么 ...

  6. V8 Javascript 引擎设计理念

    Netscape Navigator 在 90 在年代中期对 JavaScript 进行了集成,这让网页开发人员对 HTML 页面中诸如 form .frame 和 image 之类的元素的访问变得非 ...

  7. V8 javascript 引擎

    V8是一个由丹麦Google开发的开源java script引擎,用于Google Chrome中.[2]Lars Bak是这个项目的组长.[3]   V8在执行之前将java script编译成了机 ...

  8. v8引擎详解(摘)-- V8引擎是一个JavaScript引擎实现

    随着Web相关技术的发展,JavaScript所要承担的工作也越来越多,早就超越了“表单验证”的范畴,这就更需要快速的解析和执行JavaScript脚本.V8引擎就是为解决这一问题而生,在node中也 ...

  9. V8:V8(Javascript引擎)

    ylbtech-V8:V8(Javascript引擎) Lars Bak是这个项目的组长,目前该JavaScript引擎已用于其它项目的开发.第一个版本随着第一个版本的Chrome于2008年9月2日 ...

随机推荐

  1. Python序列——Unicode

    Unicode是什么 Python中的Unicode 编码与解码 在应用中使用Unicode的建议 1. Unicode是什么 Unicode是对字符进行编码的一种标准.而utf8或者utf-8是根据 ...

  2. Codeforces Round #261 (Div. 2) B. Pashmak and Flowers 水题

    题目链接:http://codeforces.com/problemset/problem/459/B 题意: 给出n支花,每支花都有一个漂亮值.挑选最大和最小漂亮值得两支花,问他们的差值为多少,并且 ...

  3. hadoop集群异常问题总结

    1. Could not find or load main class java.library.path=.opt.hadoop.lib 我的环境上是 hadoopopts变量的配置问题,至于为啥 ...

  4. SQLite多线程使用总结

    SQLite支持3种线程模式: 单线程:这种模式下,没有进行互斥,多线程使用不安全.禁用所有的mutex锁,并发使用时会出错.当SQLite编译时加了SQLITE_THREADSAFE=0参数,或者在 ...

  5. 使用boost库生成 随机数 随机字符串

    #include <iostream> #include <boost/random/random_device.hpp> #include "boost/rando ...

  6. GetModuleFileNameW

    GetModuleFileNameW( HMODULE hModule, //模块句柄 或应用程序的实例句柄 若参数为NULL,则返回该应用程序全路径 __out_ecount(nSize) LPWS ...

  7. Resistance

    题意: 给出一个由n个节点和m个二元电阻元件组成的电路,求问节点1到节点n的等效电阻. 解法: 应用电子电路分析中的基尔霍夫定律,对于每一个点有流量平衡,得 对于点$x$有 $$I_{出} + \su ...

  8. g2o使用bug总结

    g2o进行3d2d优化的时候,设置优化图的边时,注意setVertex()中顶点的顺序. void setVertex(size_t i, Vertex* v) { assert(i < _ve ...

  9. 1.5-1.6 oozie部署

    一.部署 可参考文档:http://archive.cloudera.com/cdh5/cdh/5/oozie-4.0.0-cdh5.3.6/DG_QuickStart.html 1.解压oozie ...

  10. java集合框架之Collection

    参考http://how2j.cn/k/collection/collection-collection/366.html Collection是 Set List Queue和 Deque的接口Qu ...