本文为译文,原文地址:http://v8project.blogspot.com...,做者,@Camillo Bruni ,V8 JavaScript Engine Team Bloghtml
在这篇博客中,咱们想解释 V8 如何在内部处理 JavaScript 属性。从 JavaScript 的角度来看,属性只有一些区别。JavaScript 对象主要表现为字典,字符串做为键名以及任意对象做为键值。然而,该规范在迭代过程当中对整数索引(integer-indexed
)属性和其它属性进行了不一样的处理。除此以外,不一样的属性的行为大体相同,与它们是否为整数索引无关。编程
然而,在 V8 引擎下,因为性能和内存的缘由,确实依赖于几种不一样的属性表示。在这篇博客中,咱们将介绍 V8 如何在处理动态添加的属性时提供快速的属性访问。了解属性的工做原理对于解释诸如内联缓存(inline caches
)在 V8 中的优化是相当重要的。数组
这篇博客解释了处理整数索引和命名属性(named properties
)的区别。以后咱们展现了当添加命名属性时 V8 如何维护 HiddenClasses
,便于提供一种快速的方式来识别对象的形状。而后,咱们将继续深刻了解命名属性如何针对快速访问进行优化,或依据用途进行快速修改。在最后一个章节,咱们将提供有关 V8 如何处理整数索引或数组索引(array indices
)的详细信息。缓存
咱们首先来分析一个简单的对象,如 {a: "foo", b: "bar"}
。该对象有两个命名属性,“a” 和 “b”。它是没有任何属性名称的整数索引。数组索引(array-indexed properties
)的属性(一般称为元素),在数组上最为突出。例如,数组 ["foo", "bar"]
,有两个数组索引属性:0,值为“foo”,1,值为“bar”。这是 V8 处理属性的第一个主要区别。数据结构
下图显示了一个基本的 JavaScript 对象在内存中的样子。编程语言
元素和属性存储在两个单独的数据结构中,这使得添加和访问属性或元素对于不一样的使用模式更有效。编辑器
元素主要用于各类 Array.prototype
方法,如 pop
或 slice
。假设这些函数访问连续范围内的属性,V8 大部分时间上也将它们内部表示为简单的数组。在这篇文章的后面,咱们将会解释咱们如何切换到基于稀疏字典的表示(sparse dictionary-based representation
)来节省内存。ide
命名属性以相似的方式存储在单独的数组中。然而,不一样于元素,咱们不能简单地使用键推断它所在数组中的位置,咱们须要一些额外的元数据。在 V8 中,每一个 JavaScript 对象都有一个 HiddenClass 关联。HiddenClass 存储有关对象形状的信息,其中包括从属性名到索引再到属性的映射。为了是事情复杂化,咱们有时会为属性而不是简单的数组使用字典。咱们将在专门的章节中更详细地解释这一点。函数
从这一节开始:性能
在解释元素和命名属性的区别以后,咱们须要看看 HiddenClass 在 V8 中的工做原理。HiddenClass 存储有关对象的元信息,包括对象上的属性以及对象原型的引用数量。HiddenClass 在概念上相似与典型的面向对象编程语言中的类。然而,在基于原型的语言(如 JavaScript )中,一般不可能预先知道类。所以,在这种状况下,V8 引擎的 HiddenClass 是随机建立的,并随着对象的改变而动态更新。HiddenClass 做为对象形状的标识符,而且是 V8 优化编译器和内联缓存(inline caches
)的一个很是重要的组成部分。例如,优化编辑器能够直接内联属性访问,若是它能够经过 HiddenClass 确保兼容对象结构。
让咱们来看看 HiddenClass 的重要部分。
在 V8 中,JavaScript 对象的第一个字段指向一个 HiddenClass。(实际上,这是在 V8 堆上由垃圾收集器管理的任何对象的状况)。在属性方面,最重要的信息是存储属性数量的第三位字段和指向描述符数组的指针。描述符数组包含有关命名属性的信息,如名称自己和存储值的位置。请注意,咱们在这里不跟踪整数索引属性,所以描述符数组中没有条目。
关于 HiddenClass 的基本假设是具备相同结构的对象。例如,相同的命名属性以相同的顺序共享相同的 HiddenClass。为了实现这一点,当一个属性被添加到一个对象时,咱们使用一个不一样的 HiddenClass。在下面的例子中,咱们从一个空对象开始,并添加三个命名属性。
每次添加新的属性时,对象的 HiddenClass 都会被更改。在后台 V8 建立一个将 HiddenClass 连接在一块儿的转换树。V8 知道当你向空对象添加属性“a”时要使用哪一个 HiddenClass。若是以相同的顺序添加相同的属性,则此转换树将确保最终具备相同的最终 HiddenClass。如下实例显示,即便咱们在二者之间添加简单的索引属性,也将遵循相同的转换树。
然而,若是咱们建立一个新的对象来获取不一样的属性,在这种状况下,属性“b”,V8 将为新的 HiddenClass 建立一个单独的分支。
从本节开始:
在概述 V8 如何使用 HiddenClass 跟踪对象的形状以后,让咱们来看就这些属性实际是如何存储的。如上面的介绍所述,有两种基本类型的属性:命名和索引。如下部分包含命名属性。
一个简单的对象,如 {a: 1, b: 2}
,能够在 V8 中有各类内部表现。虽然 JavaScript 的行为或多或少与外部的简单字典类似,但 V8 视图避免使用字典,由于它们阻碍了一些优化,例如内联缓存,咱们将在单独的文章中解释。
In-object 和 Normal Properties:V8 支持直接存储在对象自己上的所谓 in-object
属性。这些是 V8 中可用的最快属性。由于它们能够无间接访问。对象 in-object
的数量由对象的初始大小预先肯定。若是对象中有空格添加了更多属性,它们将被存储在属性存储中。属性存储添加了一个间接级别,但能够独立生长。
fast 和 slow 属性:下一个重要区别在于 fast
和 slow
之间的属性。一般来讲咱们将线性属性存储中存储的属性称为“fast”。fast
属性是能够简单的经过索引来访问的。要从属性的名称到属性存储中的实际位置,咱们必须先查看 HiddenClass 中的描述符数组,如前所述。
然而,若是许多属性从对象中添加和删除,则可能会生成大量时间和内存开销来维护描述符数组和 HiddenClass。所以,V8 也支持所谓的 slow
属性。具备 slow
属性的对象具备自包含的字典做为属性存储。全部属性元信息再也不存储在 HiddenClass 中的描述符数组中,而是直接存储在属性字典中。所以,能够添加和删除属性,而无需更新 HiddenClass。因为内联缓存不能与字典属性一块儿使用,后者一般比 fast
属性慢。
从这一节开始:
有三种不一样的命名属性类型:in-object
,fast
和 slow
字典。
in-object
属性直接存储在对象自己上,并提供最快访问。fast
属性存储在属性中,全部元信息都存储在 HiddenClass 的描述符数组中。slow
属性存储在自包含(self-contained)属性字典中,元信息再也不经过 HiddenClass 共享slow
属性容许有效的属性删除和添加,但访问速度比其余两种类型更慢。到目前为止,咱们已经查看了命名属性,忽略了经常使用于数组的整数索引属性。整数索引属性的处理和命名属性的复杂性相同。即便全部索引属性始终在元素存储中单独存储,也有 20 种不一样类型的元素!
Packed 或 Holey 元素:V8 作出的第一个主要区别是元素是否支持存储打包(packed)或有空位(holes)。若是你删除索引元素,或者你没有定义它,你将在后台存储中找到空位。一个简单的例子是 [1,,3],第二个条目是一个空位。下面的例子说明了这个问题:
const o = "a", "b", "c" (); console.log(o1 ()); // Prints "b". delete o1 (); // Introduces a hole in the elements store. console.log(o1 ()); // Prints "undefined"; property 1 does not exist. o.proto = {1: "B"}; // Define property 1 on the prototype. console.log(o0 ()); // Prints "a". console.log(o1 ()); // Prints "B". console.log(o2 ()); // Prints "c". console.log(o3 ()); // Prints undefined
简单来讲,若是接收方不存在属性,则必须继续查找原型链。鉴于元素是独立的,例如咱们不在 HiddenClass 上存储有关当前索引属性的信息,所以咱们须要一个名为 the_hole 的特殊值来标记不存在的属性。这对于数组很是重要。若是咱们知道没有空位,即元素存储被打包,咱们能够执行本地操做,而没必要浪费在原型链上查找。
Fast 或 Dictionary 元素:元素上第二个主要的区别是它们是 fast 仍是 dictionary 模式。fast 元素是简单的 VM 内部数组,其中属性索引映射到元素存储中的索引。然而,对于只有少数条目被占用的很是大的 sparse/holey 数组,这几乎是至关浪费的。在这种状况下,咱们使用基于字典的表示形式来节省内存,代价是访问速度稍慢:
const sparseArray = (); sparseArray9999 () = "foo"; // Creates an array with dictionary elements.
在这个例子中,使用 10k 条目分配一个完整的数组那是至关浪费的。而 V8 会建立一个字典,咱们存储一个键值描述符三元组。在这个例子中,键名会是 9999,键值为 “foo” 和默认描述符。鉴于咱们没有办法在 HiddenClass 上存储描述符详细信息,因此当你使用自定义描述符定义索引属性时,V8 将采用 slow 元素:
const array = (); Object.defineProperty(array, 0, {value: "fixed", configurable: false}); console.log(array0 ()); // Prints "fixed". array0 () = "other value"; // Cannot override index 0. console.log(array0 ()); // Still prints "fixed".
在这个例子中,咱们在数组中添加了一个不可配置的属性。该信息存储在 slow 元素字典三元组的描述符部分中。重要的是要注意,对于具备 slow 元素的对象,Array 函数执行的至关慢。
Smi 和 Double 元素:对于 fast 元素,V8 中还有另外一个重要区别。例如,若是只将数组中的整数存储在一个常见的用例中,则 GC 没必要查看数组,由于整数直接编码为所谓的小整数(Smis)。另外一个特殊状况是数组只包含 doubles。与 Smis 不一样,浮点数一般表示为占据多个单词的完整对象。然而,V8 存储纯双数组的原始双精度,以免内存和性能开销。如下示例列出了 Smis 和 double 元素的 4 个示例:
const a1 = 1, 2, 3 (); // Smi Packed const a2 = 1, , 3 (); // Smi Holey, a21 () reads from the prototype const b1 = 1.1, 2, 3 (); // Double Packed const b2 = 1.1, , 3 (); // Double Holey, b21 () reads from the prototype
特殊元素:目前为止,咱们涵盖了 20 种不一样元素中的 7 种。为了简单起见,咱们排除了 TpyedArrays 的 9 个元素类型,以及两个用于字符串包装,最后剩下两个更特殊的元素种类的参数对象。
ElementsAccessor: 能够想象,咱们并非彻底热衷于在 C++ 中编写数组函数 20 次,对于每个元素都是同样。那就是展示 C++ 魔法的时刻了,而不是一遍遍地实现 Array 函数,咱们构建了 ElementsAccessor,只须要实现从后备存储器访问元素的简单函数。ElementsAccessor 依赖于 CRTP 来建立每一个 Array 函数的专用版。所以,若是你在数组上调用 slice,V8 就会内部调用 C++ 编写的内建函数,并经过 ElementsAccessor 调用该函数的专用版本:
从这一节开始:
了解属性的工做原理是 V8 中许多优化的关键。对于 JavaScript 开发者,许多内部决策不是直接可见的,但它们解释了为何某些代码模式比其余代码模式更快。更改属性或元素类型一般会致使 V8 建立一个不一样的 HiddenClass,这可能致使相似污染,从而阻止 V8 生成最佳代码。请继续关注 V8 的内部虚拟机的工做原理。