V8中对属性的快速处理

Author Avatar
Misaka030 5月 13, 2018

在这篇文章中,我们将解释在V8内部是如何处理Javascript属性(property)的。从Javascript的角度看,不同属性之间只有少许必要的区别。Javascript对象(object)与字典(dictionary)在行为上基本相似。只有在迭代(iteration)时,对待整数索引属性(integer-indexed property)和其他属性时的规范不同。除此之外,不管是否是整数索引属性,不同属性的行为基本相同。

但是,V8需要依赖许多对属性的不同表达以达到更好的性能和内存利用。这篇文章将会解释V8如何在处理动态增加属性的同时提供快速的属性访问。理解属性的工作原理是理解优化机制(例如内联缓存 inline caches)的基础。

本文说明了V8在处理整数索引属性和命名属性(named property)时的区别。首先,我们需要了解V8是如何在增加命名属性时维护HiddenClass,以此来高效地确定object的结构。之后将会深入理解命名属性是如何根据用途来对快速访问和快速修改进行优化的。文章的最后会提供V8处理整数索引属性或数组索引(array index)的细节。

Named Properties vs. Elements

先来分析一个简单的object

1
{ a: "foo", b: "bar" }

这个object有两个命名属性a和b,没有整数索引。数组索引属性(array-indexed property, 常被称作元素(element))是数组最显著的特征。例如下面这个数组

1
["foo", "bar"]

有两个数组索引属性0和1,0代表的值是foo,1代表的值是bar。这是V8处理属性的第一个主要区别。

图一展示了基本的Javascript object在内存中的结构

A basic Javascript object looks loke in memory

图一

元素(element)和属性被存放在两个单独的数据结构中,这使得在不同的使用方式(object和array)中能够更高效地增加和访问属性或元素。

元素主要用于数组变量的方法(Array.prototype methods),比如pop和slice。考虑到这些方法都是访问连续范围内的属性,所以大部分情况下,V8内部会将它们表示为简单的数组。之后会说明有时候会切换为基于字典的表示(这种表示方法适合稀疏的数据)来节约内存。

命名属性也使用了类似的方法被存储在另一个数组中。但它不像元素,我们无法根据key来推断他们在数组中的位置。为了解决这个问题,我们需要一些额外的元数据。在V8中,每个object都附带一个HiddenClass,它存储了关于object的结构信息,除此之外,还存储了属性名称(property name)到属性中索引之间的映射关系。有时我们会使用字典来代替简单的数组,这会使处理复杂化,我们会在之后更加具体地说明这个问题。

小结

  • 数组索引属性被存放在单独的区域中(图一中的elements区域)
  • 命名属性被存放在单独的区域中(图一中的properties区域)
  • 元素和属性可以是数组或字典
  • 每个object都附带一个HiddenClass,它存储了关于object结构的信息

HiddenClass 和 DescriptorArrays

在了解元素和命名属性之间的区别之后,我们需要了解下HiddenClass是如何工作的。HiddenClass存储了关于object的元数据,包括了object中属性的数量和一个对object的属性的引用。HiddenClass在概念上类似于典型的面向对象编程语言中的类。不过在Javascript这种原型继承语言(prototype-based language)中,通常不可能事先知道类。因此在这种情况下,V8动态地创建HiddenClass,并且随着object的变化而动态更新HiddenClass。HiddenClass充当一个object结构的标识符,它是V8的优化编译器和内联缓存的重要组成部分。如果能通过HiddenClass确保object结构是兼容的,那么优化编译器实例可以直接内联访问属性。

让我们来看看HiddenClass的关键部分。

The important parts of a HiddenClass

图二

在V8中,Javascript object的第一个区域指向一个HiddenClass。(实际上,在V8中任何位于堆中并且由GC管理的object都是这样的。)对属性而言,第三个区域存放着最重要的信息,即属性的数量和一个指向描述符数组(descriptor array)的指针。描述符数组含有有关命名属性的信息,例如它自身的名称和存储值(value)的位置。需要注意的是,这里我们没有追踪整数索引属性,所以描述符数组是空的。

对HiddenClass基本的假设是具有相同结构的object共享相同的HiddenClass,例如含有相同顺序的相同命名属性的object。为了实现这个特性,我们会在object添加属性时使用一个不同的HiddenClass。下面的例子我们从一个空的object开始,向object中添加三个命名属性。

The example start from empty object and added three named properties

图三

每当添加一个新的属性,object的HiddenClass都会被改变。V8在后台创建了一个迁移树(transition tree)将HiddenClass链接在一起。V8知道当添加属性时采用哪个HiddenClass,例如,添加属性a到一个空白的object。迁移树确保在以相同顺序添加相同的属性后会得到相同的HiddenClass。下面的例子展示了即使添加简单的索引属性(indexed property),迁移树依旧保持不变。

Add simple indexed properties

图四

然而,如果我们创建一个新的object,但是添加不同的属性,V8会为新的HiddenClass创建一个独立的分支,例如在下面的例子中增加了属性d。

Add different properties

图五

小结

  • 具有相同结构(相同顺序相同属性)的object有相同的HiddenClass
  • 默认情况下,每当添加一个新的命名属性,V8就会创建一个新的HiddenClass
  • 添加数组索引属性不会创建新的HiddenClass

三种不同的命名属性

在概览了V8是如何使用HiddenClass追踪object的结构后,让我们来关注这些属性是如何存储的。正如上文中所介绍的那样,有两种基本的属性:命名属性(named)和索引属性(indexed)。随后的一节会涉及命名属性。

在V8中,一个形如 {a: 1, b: 2} 的简单的object可以有多种内部表现方式。从外部看,虽然Javascript object的行为或多或少类似于简单字典,但由于字典会影响某些优化,例如内联缓存,所以V8会试图避免字典。

In-object vs. Normal Properties

V8支持一种被称为In-object的属性,它被直接存储在object中。这是V8中可用的最高速的属性,访问它不需要任何间接手段。可以存储的In-object属性的数量取决于object的初始化大小。如果添加的属性超出了object的空间,那么它们就会被存储到properties区域中去。properties区域多了一层间接处理,但是可以独立地增长。

In-object vs. Normal Properties

图六

Fast vs. Slow Properties

快速和慢速属性(fast and slow property)之间也有重要的区别。一般来说,我们定义存储在线性的properties区域中的属性为快速属性。快速属性可以通过properties区域的索引被访问。如前所述,我们需要查询HiddenClass中的描述符数组,根据属性的名称获得它在properties区域中的实际位置。

Fast vs. Slow Properties

图七

然而,如果对一个object添加和删除大量属性,这会产生许多时间和内存的消耗来维护描述符数组和HiddenClass。因此,V8还支持慢速属性。一个使用了慢速属性的object含有一个独立的字典作为properties区域。所有属性的元数据都不再存储于HiddenClass中的描述符数组中,而是直接存储于这个properties字典中。因此,在添加和移除属性时就不需要更新HiddenClass了。由于内联缓存不适用于字典属性,所以慢速属性通常比快速属性慢。

小结

  • 三种不同的命名属性类型:In-object,快速属性和慢速属性(字典属性)

    1. In-object属性被直接存储于object中,能够提供最快的访问速度
    2. 快速属性被存储于properties区域中,而所有的元数据被存储于HiddenClass的描述符数组中
    3. 慢速属性被存储于独立的properties字典中,元数据不再通过HiddenClass共享
  • 慢速属性可以高效地移除和添加属性,但是访问速度慢于其他两种属性


未完待续