皇上,还记得我吗?我就是1999年那个Linux伊甸园啊-----24小时滚动更新开源资讯,全年无休!

JavaScript 在 V8 中的元素种类及性能优化

原文:“Elements kinds” in V8

JavaScript 对象可以具有与它们相关联的任意属性。对象属性的名称可以包含任何字符。JavaScript 引擎可以进行优化的一个有趣的例子是当属性名是纯数字时,一个特例就是数组索引的属性。

在 V8 中,如果属性名是数字(最常见的形式是 Array 构造函数生成的对象)会被特殊处理。尽管在许多情况下,这些数字索引属性的行为与其他属性一样,V8 选择将它们与非数字属性分开存储以进行优化。在引擎内部,V8 甚至给这些属性一个特殊的名称:元素。对象具有映射到值的属性,而数组具有映射到元素的索引。

尽管这些内部结构从未直接暴露给 JavaScript 开发人员,但它们解释了为什么某些代码模式比其他代码模式更快。

常见的元素种类

运行 JavaScript 代码时,V8 会跟踪每个数组所包含的元素。这些信息可以帮助 V8 优化数组元素的操作。例如,当您在数组上调用 reducemap  forEach 时,V8 可以根据数组包含哪些元素来优化这些操作。

拿这个数组举例:

它包含什么样的元素?如果你使用 typeof 操作符,它会告诉你数组包含 numbers。在语言层面,这就是你所得到的:JavaScript 不区分整数,浮点数和双精度 – 它们只是数字。然而,在引擎级别,我们可以做出更精确的区分。这个数组的元素是 PACKED_SMI_ELEMENTS。在 V8
中,术语 Smi 是指用于存储小整数的特定格式。(后面我们会在 PACKED 部分中说明。)

稍后在这个数组中添加一个浮点数将其转换为更通用的元素类型:

向数组添加字符串再次改变其元素类型。

到目前为止,我们已经看到三种不同的元素,具有以下基本类型:

  • 小整数,又称 Smi。
  • 双精度浮点数,浮点数和不能表示为 Smi 的整数。
  • 常规元素,不能表示为 Smi 或双精度的值。

请注意,双精度浮点数是 Smi 的更为一般的变体,而常规元素是双精度浮点数之上的另一个概括。可以表示为 Smi 的数字集合是可以表示为
double 的数字的子集。

这里重要的一点是,元素种类转换只能从一个方向进行:从特定的(例如 PACKED_SMI_ELEMENTS)到更一般的(例如 PACKED_ELEMENTS)。例如,一旦数组被标记为 PACKED_ELEMENTS,它就不能回到 PACKED_DOUBLE_ELEMENTS

到目前为止,我们已经学到了以下内容:

V8 为每个数组分配一个元素种类。数组的元素种类并没有被捆绑在一起 – 它可以在运行时改变。在前面的例子中,我们从 PACKED_SMI_ELEMENTS 过渡到 PACKED_ELEMENTS。元素种类转换只能从特定种类转变为更普遍的种类。

PACKED vs HOLEY

密集数组 PACKED 和稀疏数组 HOLEY

到目前为止,我们只处理密集或打包(PACKED)数组。在数组中创建稀疏数组将元素降级到其 HOLEY 变体:

V8 之所以做这个区别是因为 PACKED 数组的操作比在 HOLEY 数组上的操作更利于进行优化。对于 PACKED 数组,大多数操作可以有效执行。相比之下, HOLEY 数组的操作需要对原型链进行额外的检查和昂贵的查找。

到目前为止,我们看到的每个基本元素(即 Smis,double 和常规元素)有两种:PACKED HOLEY。我们不仅可以从 PACKED_SMI_ELEMENTS 转变为 PACKED_DOUBLE_ELEMENTS 我们也可以从任何 PACKED 形式转变成 HOLEY 形式。

回顾一下:

最常见的元素种类 PACKED  HOLEYPACKED 数组的操作比在 HOLEY 数组上的操作更为有效。元素种类可从过渡 PACKED 转变为 HOLEY

The elements kind lattice

元素种类的格

V8 将这个变换系统实现为格(数学概念)。这是一个简化的可视化,仅显示最常见的元素种类:

JavaScript 在 V8 中的元素种类及性能优化

只能通过格子向下过渡。一旦将单精度浮点数添加到 Smi 数组中,即使稍后用 Smi 覆盖浮点数,它也会被标记为 DOUBLE。类似地,一旦在数组中创建了一个洞,它将被永久标记为有洞 HOLEY,即使稍后填充它也是如此。

V8 目前有 21 种不同的元素种类,每种元素都有自己的一组可能的优化。

一般来说,更具体的元素种类可以进行更细粒度的优化。元素类型的在格子中越是向下,该对象的操作越慢。为了获得最佳性能,请避免不必要的不具体类型 – 坚持使用符合您情况的最具体的类型。

性能提示

在大多数情况下,元素种类的跟踪操作都隐藏在引擎下面,您不需要担心。但是,为了从系统中获得最大的收益,您可以采取以下几方面。再次重申:更具体的元素种类可以进行更细粒度的优化。元素类型的在格子中越是向下,该对象的操作越慢。为了获得最佳性能,请避免不必要的不具体类型 – 坚持使用符合您情况的最具体的类型。

避免创建洞(hole)

假设我们正在尝试创建一个数组,例如:

一旦数组被标记为有洞,它永远是有洞的 – 即使它被打包了!从那时起,数组上的任何操作都可能变慢。如果您计划在数组上执行大量操作,并且希望对这些操作进行优化,请避免在数组中创建空洞。V8 可以更有效地处理密集数组。

创建数组的一种更好的方法是使用字面量:

如果您提前不知道元素的所有值,那么可以创建一个空数组,然后再 push 值。

这种方法确保数组不会被转换为 holey elements。因此,V8 可以更有效地优化数组上的任何操作。

避免读取超出数组的长度

当读数超过数组的长度时,例如读取 array[42] 时,会发生类似的情况 array.length === 5。在这种情况下,数组索引 42 超出范围,该属性不存在于数组本身上,因此 JavaScript 引擎必须执行相同的昂贵的原型链查找。

不要这样写你的循环:

该代码读取数组中的所有元素,然后再次读取。直到它找到一个元素为 undefined  null时停止。(jQuery 在几个地方使用这种模式。)

相反,将你的循环写成老式的方式,只需要一直迭代到最后一个元素。

当你循环的集合是可迭代的(数组和 NodeLists),还有更好的选择:只需要使用 for-of。

对于数组,您可以使用内置的 forEach

如今,两者的性能 for-of  forEach 可以和旧式的 for 循环相提并论。

避免读数超出数组的长度!这样做和数组中的洞一样糟糕。在这种情况下,V8 的边界检查失败,检查属性是否存在失败,然后我们需要查找原型链。

避免元素种类转换

一般来说,如果您需要在数组上执行大量操作,请尝试坚持尽可能具体的元素类型,以便 V8 可以尽可能优化这些操作。

这比看起来更难。例如,只需给数组添加一个 -0,一个小整数的数组即可将其转换为 PACKED_DOUBLE_ELEMENTS

因此,此数组上的任何操作都将以与 Smi 完全不同的方式进行优化。

避免 -0,除非你需要在代码中明确区分 -0  +0。(你可能并不需要)

同样还有 NaN  Infinity。它们被表示为双精度,因此添加一个 NaN  Infinity 会将 SMI_ELEMENTS 转换为
DOUBLE_ELEMENTS

如果您计划对整数数组执行大量操作,在初始化的时候请考虑规范化 -0,并且防止 NaN 以及 Infinity。这样数组就会保持 PACKED_SMI_ELEMENTS

事实上,如果你对数组进行数学运算,可以考虑使用 TypedArray。每个数组都有专门的元素类型。

类数组对象 vs 数组

JavaScript 中的某些对象 – 特别是在 DOM 中 – 虽然它们不是真正的数组,但是他们看起来像数组。可以自己创建类数组的对象:

该对象具有 length 并支持索引元素访问(就像数组!),但它的原型上缺少数组方法,如 forEach。尽管如此,仍然可以调用数组泛型:

这个代码工作原理如下,在类数组对象上调用数组内置的 Array.prototype.forEach。但是,这比在真正的数组中调用 forEach 慢,引擎数组的 forEach 在 V8 中是高度优化的。如果你打算在这个对象上多次使用数组内置函数,可以考虑先把它变成一个真正的数组:

为了后续的优化,进行一次性转换的成本是值得的,特别是如果您计划在数组上执行大量操作。

例如,arguments 对象是类数组的对象。可以在其上调用数组内置函数,但是这样的操作将不会被完全优化,因为这些优化只针对真正的数组。

ES2015 的 rest 参数在这里很有帮助。它们产生真正的数组,可以优雅的代替类似数组的对象 arguments

如今,没有理由直接使用对象 arguments

通常,尽可能避免使用数组类对象,应该使用真正的数组。

避免多态

如果您的代码需要处理包含多种不同元素类型的数组,则可能会比单个元素类型数组要慢,因为你的代码要对不同类型的数组元素进行多态操作。

考虑以下示例,其中使用了各种元素种类调用。(请注意,这不是本机 Array.prototype.forEach,它具有自己的一些优化,这些优化不同于本文中讨论的元素种类优化。)

内置方法(如 Array.prototype.forEach)可以更有效地处理这种多态性,因此在性能敏感的情况下考虑使用它们而不是用户库函数。

V8 中单态与多态的另一个例子涉及对象形状(object shape),也称为对象的隐藏类。要了解更多,请查看 Vyacheslav 的文章

调试元素种类

找出一个给定的对象的“元素种类”,可以使用一个调试版本 d8(参见“从源代码构建”),并运行:

这将打开 d8 REPL 中的特殊函数,如 %DebugPrint(object)。输出中的“元素”字段显示您传递给它的任何对象的“元素种类”。

请注意,“COW” 表示写时复制,这是另一个内部优化。现在不要担心 – 这是另一个博文的主题!

调试版本中可用的另一个有用的标志是 --trace-elements-transitions。启用它让 V8 在任何元素发生类型转换时通知您。

如果有人让你推荐前端技术书,请让他看这个列表 ->《经典前端技术书籍
转自 http://web.jobbole.com/92547/