原文:“Elements kinds” in V8javascript
JavaScript 对象能够具备与它们相关联的任意属性。对象属性的名称能够包含任何字符。JavaScript 引擎能够进行优化的一个有趣的例子是当属性名是纯数字时,一个特例就是数组索引的属性。html
在 V8 中,若是属性名是数字(最多见的形式是 Array 构造函数生成的对象)会被特殊处理。尽管在许多状况下,这些数字索引属性的行为与其余属性同样,V8 选择将它们与非数字属性分开存储以进行优化。在引擎内部,V8 甚至给这些属性一个特殊的名称:元素。对象具备映射到值的属性,而数组具备映射到元素的索引。java
尽管这些内部结构从未直接暴露给 JavaScript 开发人员,但它们解释了为何某些代码模式比其余代码模式更快。git
运行 JavaScript 代码时,V8 会跟踪每一个数组所包含的元素。这些信息能够帮助 V8 优化数组元素的操做。例如,当您在数组上调用 reduce
,map
或 forEach
时,V8 能够根据数组包含哪些元素来优化这些操做。github
拿这个数组举例:shell
const array = [1, 2, 3];
它包含什么样的元素?若是你使用 typeof
操做符,它会告诉你数组包含 numbers
。在语言层面,这就是你所获得的:JavaScript 不区分整数,浮点数和双精度 - 它们只是数字。然而,在引擎级别,咱们能够作出更精确的区分。这个数组的元素是 PACKED_SMI_ELEMENTS。在 V8
中,术语 Smi 是指用于存储小整数的特定格式。(后面咱们会在 PACKED
部分中说明。)数组
稍后在这个数组中添加一个浮点数将其转换为更通用的元素类型:ide
const array = [1, 2, 3]; // 元素类型: PACKED_SMI_ELEMENTS array.push(4.56); // 元素类型: PACKED_DOUBLE_ELEMENTS
向数组添加字符串再次改变其元素类型。函数
const array = [1, 2, 3]; // 元素类型: PACKED_SMI_ELEMENTS array.push(4.56); // 元素类型: PACKED_DOUBLE_ELEMENTS array.push('x'); // 元素类型: PACKED_ELEMENTS
到目前为止,咱们已经看到三种不一样的元素,具备如下基本类型:性能
请注意,双精度浮点数是 Smi 的更为通常的变体,而常规元素是双精度浮点数之上的另外一个归纳。能够表示为 Smi 的数字集合是能够表示为
double 的数字的子集。
这里重要的一点是,元素种类转换只能从一个方向进行:从特定的(例如 PACKED_SMI_ELEMENTS
)到更通常的(例如 PACKED_ELEMENTS
)。例如,一旦数组被标记为 PACKED_ELEMENTS
,它就不能回到 PACKED_DOUBLE_ELEMENTS
。
到目前为止,咱们已经学到了如下内容:
V8 为每一个数组分配一个元素种类。数组的元素种类并无被捆绑在一块儿 - 它能够在运行时改变。在前面的例子中,咱们从 PACKED_SMI_ELEMENTS
过渡到 PACKED_ELEMENTS
。元素种类转换只能从特定种类转变为更广泛的种类。
密集数组 PACKED
和稀疏数组 HOLEY
。
到目前为止,咱们只处理密集或打包(PACKED
)数组。在数组中建立稀疏数组将元素降级到其 HOLEY
变体:
const array = [1, 2, 3, 4.56, 'x']; // 元素类型: PACKED_ELEMENTS array.length; // 5 array[9] = 1; // array[5] until array[8] are now holes // 元素类型: HOLEY_ELEMENTS
V8 之因此作这个区别是由于 PACKED
数组的操做比在 HOLEY
数组上的操做更利于进行优化。对于 PACKED
数组,大多数操做能够有效执行。相比之下, HOLEY
数组的操做须要对原型链进行额外的检查和昂贵的查找。
到目前为止,咱们看到的每一个基本元素(即 Smis,double 和常规元素)有两种:PACKED
和 HOLEY
。咱们不只能够从 PACKED_SMI_ELEMENTS
转变为 PACKED_DOUBLE_ELEMENTS
咱们也能够从任何 PACKED
形式转变成 HOLEY
形式。
回顾一下:
最多见的元素种类 PACKED
和 HOLEY
。PACKED
数组的操做比在 HOLEY
数组上的操做更为有效。元素种类可从过渡 PACKED
转变为 HOLEY
。
V8 将这个变换系统实现为格(数学概念)。这是一个简化的可视化,仅显示最多见的元素种类:
只能经过格子向下过渡。一旦将单精度浮点数添加到 Smi 数组中,即便稍后用 Smi 覆盖浮点数,它也会被标记为 DOUBLE
。相似地,一旦在数组中建立了一个洞,它将被永久标记为有洞 HOLEY
,即便稍后填充它也是如此。
V8 目前有 21 种不一样的元素种类,每种元素都有本身的一组可能的优化。
通常来讲,更具体的元素种类能够进行更细粒度的优化。元素类型的在格子中越是向下,该对象的操做越慢。为了得到最佳性能,请避免没必要要的不具体类型 - 坚持使用符合您状况的最具体的类型。
在大多数状况下,元素种类的跟踪操做都隐藏在引擎下面,您不须要担忧。可是,为了从系统中得到最大的收益,您能够采起如下几方面。再次重申:更具体的元素种类能够进行更细粒度的优化。元素类型的在格子中越是向下,该对象的操做越慢。为了得到最佳性能,请避免没必要要的不具体类型 - 坚持使用符合您状况的最具体的类型。
假设咱们正在尝试建立一个数组,例如:
const array = new Array(3); // 此时,数组是稀疏的,因此它被标记为 `HOLEY_SMI_ELEMENTS` // i.e. 给出当前信息的最具体的可能性。 array[0] = 'a'; // 接着,这是一个字符串,而不是一个小整数...因此过渡到`HOLEY_ELEMENTS`。 array[1] = 'b'; array[2] = 'c'; // 这时,数组中的全部三个位置都被填充,因此数组被打包(即再也不稀疏)。 // 可是,咱们没法转换为更具体的类型,例如 “PACKED_ELEMENTS”。 // 元素类保留为“HOLEY_ELEMENTS”。
一旦数组被标记为有洞,它永远是有洞的 - 即便它被打包了!从那时起,数组上的任何操做均可能变慢。若是您计划在数组上执行大量操做,而且但愿对这些操做进行优化,请避免在数组中建立空洞。V8 能够更有效地处理密集数组。
建立数组的一种更好的方法是使用字面量:
const array = ['a', 'b', 'c']; // elements kind: PACKED_ELEMENTS
若是您提早不知道元素的全部值,那么能够建立一个空数组,而后再 push
值。
const array = []; // … array.push(someValue); // … array.push(someOtherValue);
这种方法确保数组不会被转换为 holey elements。所以,V8 能够更有效地优化数组上的任何操做。
当读数超过数组的长度时,例如读取 array[42]
时,会发生相似的状况 array.length === 5
。在这种状况下,数组索引 42
超出范围,该属性不存在于数组自己上,所以 JavaScript 引擎必须执行相同的昂贵的原型链查找。
不要这样写你的循环:
// Don’t do this! for (let i = 0, item; (item = items[i]) != null; i++) { doSomething(item); }
该代码读取数组中的全部元素,而后再次读取。直到它找到一个元素为 undefined
或 null
时中止。(jQuery 在几个地方使用这种模式。)
相反,将你的循环写成老式的方式,只须要一直迭代到最后一个元素。
for (let index = 0; index < items.length; index++) { const item = items[index]; doSomething(item); }
当你循环的集合是可迭代的(数组和 NodeLists
),还有更好的选择:只须要使用 for-of。
for (const item of items) { doSomething(item); }
对于数组,您可使用内置的 forEach
:
items.forEach((item) => { doSomething(item); });
现在,二者的性能 for-of
和 forEach
能够和旧式的 for
循环相提并论。
避免读数超出数组的长度!这样作和数组中的洞同样糟糕。在这种状况下,V8 的边界检查失败,检查属性是否存在失败,而后咱们须要查找原型链。
通常来讲,若是您须要在数组上执行大量操做,请尝试坚持尽量具体的元素类型,以便 V8 能够尽量优化这些操做。
这比看起来更难。例如,只需给数组添加一个 -0
,一个小整数的数组便可将其转换为 PACKED_DOUBLE_ELEMENTS
。
const array = [3, 2, 1, +0]; // PACKED_SMI_ELEMENTS array.push(-0); // PACKED_DOUBLE_ELEMENTS
所以,此数组上的任何操做都将以与 Smi 彻底不一样的方式进行优化。
避免 -0
,除非你须要在代码中明确区分 -0
和 +0
。(你可能并不须要)
一样还有 NaN
和 Infinity
。它们被表示为双精度,所以添加一个 NaN
或 Infinity
会将 SMI_ELEMENTS
转换为DOUBLE_ELEMENTS
。
const array = [3, 2, 1]; // PACKED_SMI_ELEMENTS array.push(NaN, Infinity); // PACKED_DOUBLE_ELEMENTS
若是您计划对整数数组执行大量操做,在初始化的时候请考虑规范化 -0
,而且防止 NaN
以及 Infinity
。这样数组就会保持 PACKED_SMI_ELEMENTS
。
事实上,若是你对数组进行数学运算,能够考虑使用 TypedArray
。每一个数组都有专门的元素类型。
JavaScript 中的某些对象 - 特别是在 DOM 中 - 虽然它们不是真正的数组,可是他们看起来像数组。能够本身建立类数组的对象:
const arrayLike = {}; arrayLike[0] = 'a'; arrayLike[1] = 'b'; arrayLike[2] = 'c'; arrayLike.length = 3;
该对象具备 length 并支持索引元素访问(就像数组!),但它的原型上缺乏数组方法,如 forEach
。尽管如此,仍然能够调用数组泛型:
Array.prototype.forEach.call(arrayLike, (value, index) => { console.log(`${ index }: ${ value }`); }); // This logs '0: a', then '1: b', and finally '2: c'.
这个代码工做原理以下,在类数组对象上调用数组内置的 Array.prototype.forEach
。可是,这比在真正的数组中调用 forEach
慢,引擎数组的 forEach
在 V8 中是高度优化的。若是你打算在这个对象上屡次使用数组内置函数,能够考虑先把它变成一个真正的数组:
const actualArray = Array.prototype.slice.call(arrayLike, 0); actualArray.forEach((value, index) => { console.log(`${ index }: ${ value }`); }); // This logs '0: a', then '1: b', and finally '2: c'.
为了后续的优化,进行一次性转换的成本是值得的,特别是若是您计划在数组上执行大量操做。
例如,arguments
对象是类数组的对象。能够在其上调用数组内置函数,可是这样的操做将不会被彻底优化,由于这些优化只针对真正的数组。
const logArgs = function() { Array.prototype.forEach.call(arguments, (value, index) => { console.log(`${ index }: ${ value }`); }); }; logArgs('a', 'b', 'c'); // This logs '0: a', then '1: b', and finally '2: c'.
ES2015 的 rest 参数在这里颇有帮助。它们产生真正的数组,能够优雅的代替相似数组的对象 arguments
。
const logArgs = (...args) => { args.forEach((value, index) => { console.log(`${ index }: ${ value }`); }); }; logArgs('a', 'b', 'c'); // This logs '0: a', then '1: b', and finally '2: c'.
现在,没有理由直接使用对象 arguments
。
一般,尽量避免使用数组类对象,应该使用真正的数组。
若是您的代码须要处理包含多种不一样元素类型的数组,则可能会比单个元素类型数组要慢,由于你的代码要对不一样类型的数组元素进行多态操做。
考虑如下示例,其中使用了各类元素种类调用。(请注意,这不是本机 Array.prototype.forEach
,它具备本身的一些优化,这些优化不一样于本文中讨论的元素种类优化。)
const each = (array, callback) => { for (let index = 0; index < array.length; ++index) { const item = array[index]; callback(item); } }; const doSomething = (item) => console.log(item); each([], () => {}); each(['a', 'b', 'c'], doSomething); // `each` is called with `PACKED_ELEMENTS`. V8 uses an inline cache // (or “IC”) to remember that `each` is called with this particular // elements kind. V8 is optimistic and assumes that the // `array.length` and `array[index]` accesses inside the `each` // function are monomorphic (i.e. only ever receive a single kind // of elements) until proven otherwise. For every future call to // `each`, V8 checks if the elements kind is `PACKED_ELEMENTS`. If // so, V8 can re-use the previously-generated code. If not, more work // is needed. each([1.1, 2.2, 3.3], doSomething); // `each` is called with `PACKED_DOUBLE_ELEMENTS`. Because V8 has // now seen different elements kinds passed to `each` in its IC, the // `array.length` and `array[index]` accesses inside the `each` // function get marked as polymorphic. V8 now needs an additional // check every time `each` gets called: one for `PACKED_ELEMENTS` // (like before), a new one for `PACKED_DOUBLE_ELEMENTS`, and one for // any other elements kinds (like before). This incurs a performance // hit. each([1, 2, 3], doSomething); // `each` is called with `PACKED_SMI_ELEMENTS`. This triggers another // degree of polymorphism. There are now three different elements // kinds in the IC for `each`. For every `each` call from now on, yet // another elements kind check is needed to re-use the generated code // for `PACKED_SMI_ELEMENTS`. This comes at a performance cost.
内置方法(如 Array.prototype.forEach
)能够更有效地处理这种多态性,所以在性能敏感的状况下考虑使用它们而不是用户库函数。
V8 中单态与多态的另外一个例子涉及对象形状(object shape),也称为对象的隐藏类。要了解更多,请查看 Vyacheslav 的文章。
找出一个给定的对象的“元素种类”,可使用一个调试版本 d8(参见“从源代码构建”),并运行:
$ out.gn/x64.debug/d8 --allow-natives-syntax
这将打开 d8 REPL 中的特殊函数,如 %DebugPrint(object)
。输出中的“元素”字段显示您传递给它的任何对象的“元素种类”。
d8> const array = [1, 2, 3]; %DebugPrint(array); DebugPrint: 0x1fbbad30fd71: [JSArray] - map = 0x10a6f8a038b1 [FastProperties] - prototype = 0x1212bb687ec1 - elements = 0x1fbbad30fd19 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)] - length = 3 - properties = 0x219eb0702241 <FixedArray[0]> { #length: 0x219eb0764ac9 <AccessorInfo> (const accessor descriptor) } - elements= 0x1fbbad30fd19 <FixedArray[3]> { 0: 1 1: 2 2: 3 } […]
请注意,“COW” 表示写时复制,这是另外一个内部优化。如今不要担忧 - 这是另外一个博文的主题!
调试版本中可用的另外一个有用的标志是 --trace-elements-transitions
。启用它让 V8 在任何元素发生类型转换时通知您。
$ cat my-script.js const array = [1, 2, 3]; array[3] = 4.56; $ out.gn/x64.debug/d8 --trace-elements-transitions my-script.js elements transition [PACKED_SMI_ELEMENTS -> PACKED_DOUBLE_ELEMENTS] in ~+34 at x.js:2 for 0x1df87228c911 <JSArray[3]> from 0x1df87228c889 <FixedArray[3]> to 0x1df87228c941 <FixedDoubleArray[22]>