读取数组的`length`属性真的在JavaScript中花费那么大的代价吗?


68

我一直认为在JavaScript中缓存数组的长度是一个好主意(尤其是在for循环条件下),因为计算数组长度的代价很高。

for (var i = 0; i < arr.length; i++) { }

// vs

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

但是,我认为也许length只有在创建和更改数组时才更新该属性。因此,与读取存储在变量中的操作相比,读取它的操作不会太昂贵(与其他语言的其他方法相反,可能需要在内存中查找以找到某事物的结尾,例如strlen()在C语言中)。

我有两个问题。我也对它的工作方式感兴趣,所以请不要用过早的优化工具打我。

假设浏览器中的JavaScript引擎。

  1. length在JavaScript中缓存数组的属性有什么好处吗?在读取对象属性的局部变量时还有更多的事情吗?
  2. length属性是否仅在创建时以及在不返回新数组的情况下使用onshift()pop()type方法更改,否则仅存储为整数?

Answers:


49

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

我怀疑长度存储为整数(Uint32)。从ECMA规范(第262版第5页,第121页)中:

每个Array对象都有一个length属性,其值始终是小于2的非负整数 32。length属性的值在数值上大于名称为数组索引的每个属性的名称;每当创建或更改Array对象的属性时,都会根据需要调整其他属性以保持该不变性。具体来说,每当添加一个名称为数组索引的属性时,如果需要,将length属性更改为比该数组索引的数值大一的值;并且只要更改length属性,就会自动删除名称为数组索引且其值不小于新长度的每个属性。此约束仅适用于Array对象的自身属性,不受可能从其原型继承的长度或数组索引属性的影响

!我不知道我是否曾经习惯过这种语言...

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


在我的Linux i686上的Chrome 10.0.648.127中,它也没有关系。
加比·普卡鲁

5
脚本语言(Ruby,Perl等)中的数组通常知道两个长度作为简单整数:已分配了多少个插槽以及正在使用多少个插槽(即长度)。任何可能在JavaScript引擎中以不同方式执行此操作的人,都不应被允许在编译器附近的任何地方使用:)数组通常会知道不进行计算有多长时间。当然除了C。
亩太短了,

1
@Joachim Sauer。假设是真的。无论如何,为什么要编写简单明了的代码作为Java规则?
KooiInc 2011年

1
不仅是Java规则,而且在Java中有时出于性能原因也被引用。有一篇经常被引用的文章,名为“ Write Dumb Code”
约阿希姆·绍尔

4
我要说的是,由于缓存长度确实会在IE和IE中产生影响(至少在IE 9之前),因此浏览器是最慢的浏览器,因此这是值得进行的优化。
蒂姆·唐

28

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_elementsset_bit_field2(bit_field2() | (1 << kHasFastElements)))中设置了一个位标记,这是我在Google代码中查找时要绘制挖掘线的地方,而本地没有源代码。

现在,它似乎任何时候任何操作是在阵列上完成(这是一个子类的JSObject,就会调用作出NormalizeElements(),其执行以下操作:

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

因此,在回答您的问题时:

  1. Chrome(或其他使用V8的浏览器)在缓存length数组的属性方面似乎没有任何优势(除非您做一些奇怪的事情将其强制为slow(我不确定这些条件是什么) )-话虽如此,我很可能会继续缓存,length直到我有机会遍历所有浏览OS浏览器的实现;)
  2. 在对象上执行任何操作length后,该属性似乎已更改。

编辑:

附带说明一下,似乎“空”数组实际上已分配为具有4个元素:

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

我不知道数组有多少元素增长由曾经的范围已经超出了-我没有挖的是深:)


典型的实现将使数组的大小增长恒定的倍数-通常通过加倍,三倍,四倍等来实现。所有这些都O(1)为插入产生相同的摊销额。
Matt Ball

@马特:谢谢-会想出尽可能多的,但是没有知道肯定,因为我没有深入挖掘:)
德棉布莱希特

7
那是一个很长的tl; dr,但用于挖掘浏览器源代码却是+1。
卡米洛·马丁

13

另一套性能测试。该循环是在具有空循环的一百万个随机数数组上完成的。

在Chrome中,具有缓存长度和非缓存长度的循环几乎同时计时,所以我猜想这是V8优化来缓存长度的。

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


在Firefox 40中,缓存版本和非缓存版本具有相同的性能。
MichaelGroße2015年

7

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

Grinch如何通过Vyacheslav Egorov窃取array.length

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

但是无论如何,这种微优化不可能为您的用户带来任何明显的收益。为了他们和您的利益,请专注于易于阅读的代码,并在代码中使用良好的数据结构和算法!

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


4

请注意:

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

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

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


有趣的是,我只是在主要的四种浏览器(现在已包含在图表中)中进行了此测试,并且存储长度在IE和Safari中非常明显,但是在Chrome和Firefox中却没有任何好处。令人惊讶的是,Firefox将其他浏览器吹走了。毫不奇怪,Safari在我的测试中是最慢的。
pickypg

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.