【译】V8 引擎怎样对属性进行快速访问

V8 引擎怎样对属性进行快速访问

在这篇文章中我将要解释 V8 引擎内部是如何处理 JavaScript 属性的。从 JavaScript 的角度来看,属性们区别并不大,JavaScript 对象表现形式更像是字典,字符串做为键,任意对象做为值。ECMAScript 语言规范 中,对象的数字索引和其余类型索引在规范中没有明确区分,可是在 V8 引擎内部却不是这样的。除此以外,不一样属性的行为基本相同,和他们可不能够进行整数索引没有关系。html

然而在 V8 引擎中属性的不一样表现形式确实会对性能和内存有影响,在这篇文章中咱们来解析 V8 引擎是如何可以在动态添加属性时进行快速的属性访问的,理解属性是如何工做的,以解释 V8 引擎是如何的优化,(例如 内联缓存 )。前端

这篇文章解释了处理整数索引属性和命名属性的不一样之处,以后咱们展现了 V8 中是如何为了提供一个快速的方式定义一个对象的模型在添加一个命名属性时使用 HiddenClasses。而后,咱们将继续深刻了解如何根据使用状况进行属性名的命名优化,以便可以快速访问或者快速修改。在最后一节中,咱们介绍 V8 如何处理整数索引属性或数组索引的详细信息。 react

命名属性和元素

让咱们从分析一个很是简单的对象开始,好比:{a: "foo", b: "bar"}。这个对象有两个命名属性,"a" 和 "b"。它没有使用任何的整数索引做为属性名。咱们也可使用索引访问属性,特别是对象为数组的状况。例如,数组 ["foo", "bar"] 有两个可使用数组索引的属性:索引为 0 的值是 "foo",索引为 1 的值是 "bar"android

这是 V8 通常处理属性的第一个主要区别。ios

下图显示了一个 JavaScript 的基本对象在内存中的样子。git

元素和属性存储在两个独立的数据结构中,这使得使用不一样的模式添加和访问属性和元素将会更加高效。github

元素主要用于各类 Array.prototype methods 例如 popslice。考虑到这些函数是在连续范围存储区域内访问属性的,V8 引擎内部大部分状况下也将他们表示为简单的数组。稍后咱们将解释如何使用一个稀疏的基于字典的表示来节省内存。编程

命名属性的存储相似于稀疏数组的存储。然而,与元素不一样,咱们不能简单的使用键推断其在属性数组中的位置,咱们须要一些额外的元数据。在 V8 中,每个 JavaScript 对象都有一个相关联的 HiddenClass。这个 HiddenClass 存储了一个对象的模型信息,在其余方面,有一个从属性名到属性索引映射。咱们有时使用一个字典来代替简单的数组。咱们专门会在一个章节中更详细地解释这一点。后端

本节重点:数组

  • 数组索引属性存储在单独的元素存储区中。
  • 命名属性存储在属性存储区中。
  • 元素和属性能够是数组或字典。
  • 每一个 JavaScript 对象有一个和对象的模型相关联的 HiddenClass

HiddenClasses 和描述符数组

在介绍了元素和命名属性的大体区别以后,咱们须要来看一下 HiddenClasses 在 V8 中是怎么工做的。HiddenClass 存储了一个对象的元数据,包括对象和对象引用原型的数量。HiddenClasses 在典型的面向对象的编程语言的概念中和“类”相似。然而,在像 JavaScript 这样的基于原型的编程语言中,通常不可能预先知道类。所以,在这种状况下,在 V8 引擎中,HiddenClasses 建立和更新属性的动态变化。HiddenClasses 做为一个对象模型的标识,而且是 V8 引擎优化编译器和内联缓存的一个很是重要的因素。经过 HiddenClass 能够保持一个兼容的对象结构,这样的话实例能够直接使用内联的属性。

让咱们来看一下 HiddenClass 的重点

在 V8 中,JavaScript 对象的第一部分就是指向 HiddenClass。(实际上,V8 中的任何对象都在堆中而且受垃圾回收器管理。)在属性方面,最重要的信息是第三段区域,它存储属性的数量,以及一个指向描述符数组的指针。描述符数组包含有关命名属性的信息,如名称自己和存储值的位置。注意,咱们不在这里跟踪整数索引属性,所以描述符数组中没有整数索引的条目。

关于 HiddenClasses 的基本假设是对象具备相同的结构,例如,相同的顺序对应相同的属性,共用相同的 HiddenClass。当咱们给一个对象添加一个属性的时候咱们使用不一样的 HiddenClass 实现。在下面的例子中,咱们从一个空对象开始而且添加三个命名属性。

每次加入一个新属性时,对象的 HiddenClass 就会改变,在 V8 引擎的后台会建立一个将 HiddenClass 链接在一块儿的转移树。V8 引擎就知道你添加的 HiddenClass 是哪个了,例如,属性 “a” 添加到一个空对象中,若是你以相同的顺序添加相同的属性,这个转化树会使用相同的 HiddenClass。下面的示例代表,即便在二者之间添加简单的索引属性,咱们也将遵循相同的转换树。

本节重点:

  • 结构相同的对象(相同的顺序对于相同的属性)有相同的 HiddenClasses。
  • 默认状况下,每添加一个新的命名属性将产生了一个新的 HiddenClasses。
  • 增长数组索引属性并不创造新 HiddenClasses。

三种不一样的命名属性

在概述了 V8 引擎是如何使用 HiddenClasses 来追踪对象的模型以后,咱们来看一下这些属性其实是如何储存的。正如上面介绍所介绍的,有两种基本属性:命名属性和索引属性。如下部分是命名属性:

一个简单的对象,例如 {a: 1, b: 2} 在 V8 引擎的内部有多种表现形式,虽然 JavaScript 对象或多或少的和外部的字典类似,V8 引擎仍然试图避免和字典相似由于他们妨碍某些优化,例如 内联缓存,咱们将在一篇单独的文章中解释。

In-object 属性和通常属性: V8 引擎支持直接储存在所谓的 In-object 的属性。这些是 V8 引擎中可用的最快速的属性,由于他们能够直接访问。In-object 属性的数量是由对象的初始大小决定的。若是在对象中添加超出存储空间的属性,那么他们会储存在属性存储区中。属性存储多了一层间接寻址但这是独立的区域。

快属性 VS 慢属性: 下一个重要的区别来自于快属性和慢属性。一般,咱们将存储在线性属性存储区域的属性称为快属性。快属性仅经过属性存储区的索引访问,为了在属性存储区的实际位置获得属性的名字,咱们必须经过在 HiddenClass 中的描述符数组。

然而,从一个对象中添加或删除多个属性,会为了保持描述符数组和 HiddenClasses 而产生大量的时间和内存的开销。所以,V8 引擎也支持所谓的慢属性,一个有慢属性的对象有一个自包含的字典做为属性存储区。全部的属性元数据都再也不存储在 HiddenClass 的描述符数组而是直接在属性字典。所以,属性能够添加和删除不更新的 HiddenClass。因为内联缓存不使用字典属性,后者一般比快速属性慢。

本节重点:

  1. 有三种不一样的命名属性类型:对象、快字典和慢字典。
  • 在对象属性中直接存储在对象自己上,并提供最快的访问速度。
  • 快属性存储在属性存储区,全部的元数据存储在 HiddenClass 的描述符数组中。
  • 慢属性存储在自身的属性字典中,元数据再也不存储于 HiddenClass。
  1. 慢属性容许高效的属性删除和添加,但访问速度比其余两种类型慢。

元素或数组索引属性

到目前为止,咱们已经了解了命名属性,在研究的过程当中忽略数组中经常使用的整数索引属性。处理整数索引属性并不比命名属性简单。虽然全部的索引属性老是单独存放在元素存储中,可是有 20 种不一样类型的元素!

元素是连续的的仍是有缺省的: V8 引擎的第一个主要区别是元素在存储区是连续的仍是有缺省的。若是删除索引元素,或者在不定义索引元素的状况下,就会在存储区中有一个缺省。一个简单的例子是 [1,,3],第二个位置缺省。下面的例子说明了这个问题:

const o = ["a", "b", "c"];
console.log(o[1]);          // 打印 "b".

delete o[1];                // 删除一个属性.
console.log(o[1]);          // 打印 "undefined"; 第二个属性不存在
o.__proto__ = {1: "B"};     // 在原型上定义第二个属性

console.log(o[0]);          // 打印 "a".
console.log(o[1]);          // 打印 "B".
console.log(o[2]);          // 打印
console.log(o[3]);          // 打印 undefined复制代码

简言之,若是接收器上不存在属性,咱们必须继续在原型链上查找。若是元素是自包含的,咱们不在 HiddenClass 中存储有关当前索引的属性,咱们须要一个特殊的值,称为 the_hole,来标记该位置的属性是不存在的。这个数组函数的性能是相当重要的。若是咱们知道有没有缺省,即元素是连续的,咱们能够不用昂贵代价来查询原型链来进行本地操做。

快速元素和字典元素: 元素的第二个主要区别是它们是快速的仍是字典模式的。快速元素是简单的 VM 内部数组,其中属性索引映射到元素存储区中的索引。然而,这种简单的表示在稀疏数组中是至关浪费的。在这种状况下,咱们使用基于字典的表示来节省内存,以访问速度稍微慢一些为代价:

const sparseArray = [];
sparseArray[1 << 20] = "foo"; // 使用字典元素建立一个数组。复制代码

在这个例子中,若是分配一个 10K 的全排列会更浪费。因此取而代之的是 V8 建立的一个字典,咱们在其中存储三个如出一辙的键值描述符。本例中的键为 10000,值为“字符串”还有一个默认描述符。由于咱们没有办法在 HiddenClass 存储区描述细节,在 V8 中 当你定义一个索引属性与自定义描述符存储在慢元素中:

const array = [];
Object.defineProperty(array, 0, {value: "fixed", configurable});
console.log(array[0]);      // 打印 "fixed".
array[0] = "other value";   // 不能从新第 1 个索引.
console.log(array[0]);      // 仍然打印 "fixed".复制代码

在这个例子中,咱们在数组上添加了一个 configurablefalse 的属性。此信息存储在慢元素字典三元组的描述符部分中。须要注意的是,在慢元素对象上,数组函数的执行速度要慢得多。

小整数和双精度元素: 对于快速元素,V8中还有另外一个重要的区别。例如,若是你只保存整数数组,一个常见的例子:GC 没有接受数组,由于整数直接编码为所谓的小整数(SMIS)。另外一个特例是数组,它们只包含双精度数。不像SMIS,浮点数一般表示为对象占用的几个字符。然而,V8 使用两行来存储纯双精度组,以免内存和性能开销。下面的示例列出了 SMI 和双精度元素的 4 个示例:

const a1 = [1,   2, 3];  // Smi Packed
const a2 = [1,    , 3];  // Smi Holey, a2[1] reads from the prototype
const b1 = [1.1, 2, 3];  // Double Packed
const b2 = [1.1,  , 3];  // Double Holey, b2[1] reads from the prototype复制代码

特别的元素: 到目前为止,咱们涵盖了 20 种不一样元素中的 7 种。为简单起见,咱们排除了 9 元种 数组类型,两个字符串包装等等,两个参数对象。

ElementsAccessor: 你能够想象咱们并不想为了每一种元素在 C++ 中写 20 次数组函数。这就是 C++ 的奇妙之处。为了代替一次又一次数组函数的实现,咱们在从后备存储访问元素创建了 ElementsAccessor 。ElementsAccessor 依赖 CRTP 建立每个数组函数的专业版。因此,若是你调用数组中的一些方法例如 slice,将经过调用 V8 引擎的内部调用内置 C++ 编写的,ElementsAccessor 的专业版:

本节重点:

  • 有快速模式和字典模式索引属性和元素。
  • 快速属性能够被打包而且他们能够包含被删除索引属性缺省的标志。
  • 数组元素类型固定,以加速数组函数并减小 GC 开销,方便引擎优化。

了解属性如何工做是在 V8 中许多优化的关键。对于 JavaScript 开发人员来讲,这些内部决策中有不少是不可见的,但它们解释了为何某些代码模式比其余代码模式更快。更改属性或元素类型一般让 V8 创造不一样的 HiddenClass,阻碍 V8 优化的缘由。敬请期待我之后的文章:V8 引擎 VM 内部是如何工做的。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索