在 JavaScript 中读取数组的 `length` 属性真的那么昂贵吗?

IT技术 javascript arrays micro-optimization
2021-01-29 14:58:37

我一直认为在 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属性只是改变对创造和shift()pop()类型的方法不返回一个新的数组否则简单地存储为整数?
6个回答

好吧,我会说它很贵,但后来我写了一个小测试@ jsperf.com,令我惊讶的i<array.length是,在 Chrome 中使用实际上更快,而在 FF(4) 中这并不重要。

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

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

呼!我不知道我是否习惯了这种语言......

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

@约阿希姆绍尔。假设这是真的。无论如何,为什么要把编写简单、直接的代码称为 Java 规则?
2021-03-15 14:58:37
在我的 Linux i686 上的 Chrome 10.0.648.127 中,这也无关紧要。
2021-03-23 14:58:37
脚本语言(Ruby、Perl 等)中的数组通常知道两个长度作为简单的整数:已分配多少个插槽和使用了多少个插槽(即长度)。任何在 JavaScript 引擎中以不同方式执行的人可能都不应该被允许在编译器附近的任何地方:) 数组通常知道它们在不计算的情况下有多长。当然,C 除外。
2021-03-25 14:58:37
我想说的是,由于缓存长度确实在 IE 和 IE(至少在 IE 9 之前)是最慢的浏览器中有所不同,因此这是一项值得进行的优化。
2021-03-25 14:58:37
不仅是 Java 规则,而且在 Java 中有时会出于性能原因引用它有一篇经常被引用的文章,标题为“编写愚蠢的代码”
2021-03-31 14:58:37

特尔;博士:

据我所知,数组的长度似乎是在内部缓存的(至少在 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)))中设置了一个位标志,这是我想在我查看 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直到我有机会完成所有操作系统浏览器实现;)
  2. 在对对象length进行任何操作后,属性似乎已更改

编辑:

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

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

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

这是一个很长的 tl;dr,但是 +1 用于挖掘浏览器源代码。
2021-03-14 14:58:37
@马特:谢谢-会想出尽可能多的,但是没有知道肯定,因为我没有深入挖掘:)
2021-03-27 14:58:37
典型的实现将数组增长为数组大小的常数倍 - 通常通过加倍、三倍、四倍等,所有产生相同O(1)的插入摊销
2021-04-12 14:58:37

另一组性能测试循环是在一个包含空循环的百万随机数数组上完成的。

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

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

在 Firefox 40 中,缓存版本和非缓存版本具有相同的性能。
2021-03-19 14:58:37

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

Grinch 如何窃取Vyacheslav Egorov 的array.length 访问权限

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

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

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

只是一个注意事项:

在某些浏览器上(我在 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 在我的测试中是迄今为止最慢的。
2021-03-29 14:58:37