在JavaScript中读取数组的' length '属性是否真的是一个昂贵的操作

Is reading the `length` property of an array really that expensive an operation in JavaScript?

本文关键字:操作 一个 属性 读取 JavaScript 数组 length 是否 真的      更新时间:2023-09-26

我一直认为在JavaScript中缓存数组的长度是一个好主意(特别是在for循环的情况下),因为计算数组长度的开销很大。

例子
for (var i = 0; i < arr.length; i++) { }
// vs
for (var i = 0, arrLength = arr.length; i < arrLength; i++) { }

然而,我认为length属性可能只在创建和更改数组时更新。因此,与读取存储在变量中的数据相比,读取它不应该是一个太昂贵的操作(与其他语言中的其他方法相反,这些方法可能需要在内存中查找某些内容的结尾,例如C中的strlen())。

我有两个问题。我对这是如何工作的也很感兴趣,所以请不要用过早优化棍子打我。

假设浏览器中的JavaScript引擎。

  1. 在JavaScript中缓存数组的length属性是否有任何优势?在对象的属性上读取局部变量是否涉及更多?
  2. length属性简单地改变创建和shift()pop()类型的方法,不返回一个新的数组,否则简单地存储为一个整数?

嗯,我本来会说这是昂贵的,但后来我写了一个小测试@ jsperf.com,令我惊讶的是,使用i<array.length实际上在Chrome中更快,在FF(4)中没关系。

我的怀疑是长度存储为整数(Uint32)。来自ecma规范(262版5,第121页):

每个Array对象都有一个长度属性,其值总是小于232的非负整数。长度属性的值为的名称在数字上大于每个名称为数组的属性指数;数组的属性对象被创建或更改时,其他根据需要调整属性保持这个不变量。具体来说,每当属性是添加了名称为数组索引的对象,长度属性被改变,如果必要的,要比该数组索引的数值;和只要长度属性是更改了,所有名称为的属性数组索引,其值为小于新的长度自动删除。这个约束类的自身属性数组对象,不受长度或数组索引属性可以从它的原型继承

唷!我不知道我是否习惯了这样的语言。

最后,我们的老浏览器总是落后的。在IE(9,8,7)中缓存长度确实更快。我说,这是不使用IE的众多原因之一。

TL;DR:

从我能收集到的,它似乎像数组的长度是内部缓存的(至少在V8)..

(细节?继续阅读:))

所以,这个问题在我的脑海里萦绕了好几次,我决定找到问题的根源(至少,在一个实现中)。

挖掘V8源代码产生了JSArray类。

// The JSArray describes JavaScript Arrays
//  Such an array can be in one of two modes:
//    - fast, backing storage is a FixedArray and length <= elements.length();
//       Please note: push and pop can be used to grow and shrink the array.
//    - slow, backing storage is a HashTable with numbers as keys.

我假设数组元素的类型决定了它是快还是慢。我在set_has_fast_elements (set_bit_field2(bit_field2() | (1 << kHasFastElements)))中设置了一个位标志,这就是我在谷歌代码中寻找并且没有本地源代码时,我想我会绘制挖掘线的地方。

现在,似乎任何时间任何操作都是在数组(它是JSObject的子类)上完成的,调用NormalizeElements(),执行以下操作:

// Compute the effective length.
  int length = IsJSArray() ?
      Smi::cast(JSArray::cast(this)->length())->value() :
      array->length();

那么,在回答你的问题时:

  1. 似乎没有任何优势在Chrome(或其他浏览器使用V8)缓存一个数组的length属性(除非你做一些奇怪的事情,将迫使它是slow(我不确定这些条件是什么)-已经说过,我很可能会继续缓存length,直到我有机会通过所有操作系统浏览器实现;)
  2. 对对象进行任何操作后,length属性似乎被改变了。
编辑:

另一方面,似乎一个"空"数组实际上被分配为有4个元素:

// Number of element slots to pre-allocate for an empty array.
static const int kPreallocatedArrayElements = 4;

我不确定一旦超出边界,数组会增长多少元素-我没有深入挖掘:)

另一组性能测试。循环是在一个包含一百万个随机数的数组上进行的,其中有一个空循环。

在Chrome中,缓存和非缓存长度的循环时钟几乎相同的时间,所以我猜这是V8优化缓存长度。

在Safari和Firefox中,缓存的长度始终比未缓存的版本快2倍。

本文通过向IRHydra请求生成的代码来研究V8和Chrome中的自动缓存:

圣诞怪杰是如何偷走数组的。Vyacheslav Egorov访问长度

他发现在某些条件下手动缓存.length实际上增加了开销而不是提高了性能!

但无论如何,这种微优化不太可能为您的用户获得任何明显的收益。为了他们的利益,也为了你自己的利益,你应该把重点放在易于阅读的代码上,并在代码中使用良好的数据结构和算法!

避免过早优化:专注于优雅的代码,直到出现性能问题。只有这样,才能通过分析找出瓶颈,然后优化代码的那部分

提醒一下:

在一些浏览器上(我注意到在Safari, IE和Opera中),您可以通过缓存for循环声明中的长度来提高速度:

var j;
for (var i = 0, len = arr.length; i < len; i++) {
  j = arr[i];
}

我编辑了@KooiInc上面的jsperf测试来添加这种情况。

注意不要假设所有可迭代集合都是如此。例如,缓存HTMLCollection的长度在Chrome(版本41)中快65%,在Firefox(版本36)中快35%。

http://jsperf.com/array-length-in-loop-dom