为什么array.push有时比array [n] = value更快?

时间:2009-03-05 09:41:59

标签: javascript arrays performance firefox browser

作为测试一些代码的副作用,我写了一个小函数来比较使用array.push方法和直接寻址(array [n] = value)的速度。令我惊讶的是,推送方法通常表现得更快,特别是在Firefox中,有时在Chrome中。只是出于好奇:任何人都有解释吗? 您可以找到测试@ this page(单击“数组方法比较”)

6 个答案:

答案 0 :(得分:80)

各种各样的因素都会发挥作用,大多数JS实现都会使用一个平面数组,如果以后需要它就会转换为稀疏存储。

基本上,变稀疏的决定是基于正在设置什么元素的启发式算法,以及为了保持平坦而浪费多少空间。

在您的情况下,您首先设置最后一个元素,这意味着JS引擎将看到一个数组需要长度为n但只有一个元素。如果n足够大,这将立即使数组成为稀疏数组 - 在大多数引擎中,这意味着所有后续插入都将采用慢稀疏数组的情况。

你应该添加一个额外的测试,在其中你将数组从索引0填充到索引n-1 - 它应该更快,更快。

为了回应@Christoph并且出于拖延的愿望,这里描述了如何(通常)在JS中实现数组 - 具体内容从JS引擎到JS引擎不等,但一般原理是相同的。

所有JS Object(所以不是字符串,数字,true,false,undefinednull)都从基础对象类型继承 - 确切的实现方式各不相同,它可以是C ++继承,或者在C中手动(以任何方式执行它都有好处) - 基础对象类型定义默认属性访问方法,例如。

interface Object {
    put(propertyName, value)
    get(propertyName)
private:
    map properties; // a map (tree, hash table, whatever) from propertyName to value
}

此Object类型处理所有标准属性访问逻辑,原型链等。 然后,Array实现变为

interface Array : Object {
    override put(propertyName, value)
    override get(propertyName)
private:
    map sparseStorage; // a map between integer indices and values
    value[] flatStorage; // basically a native array of values with a 1:1
                         // correspondance between JS index and storage index
    value length; // The `length` of the js array
}

现在,当您在JS中创建数组时,引擎会创建类似于上述数据结构的内容。当您将对象插入Array实例时,Array的put方法会检查属性名称是否为0到2 ^ 32之间的整数(或者可以转换为整数,例如“121”,“2341”等) -1(或者可能是2 ^ 31-1,我完全忘了)。如果不是,则将put方法转发到基础Object实现,并完成标准[[Put]]逻辑。否则将值放入Array自己的存储器中,如果数据足够紧凑则引擎将使用平面阵列存储,在这种情况下插入(和检索)只是标准的数组索引操作,否则引擎将转换数组稀疏存储,并使用map来获取propertyName到value location。

我真的不确定在转换发生后,目前是否有任何JS引擎从稀疏存储转换为平面存储。

Anyhoo,这是一个相当高级别的概述,发生了什么,并留下了一些更icky的细节,但这是一般的实施模式。有关如何调度附加存储以及如何调度put / get的细节因引擎而异 - 但这是我能够真正描述设计/实现的最清晰的。

一个小的加法点,而ES规范将propertyName称为字符串JS引擎也倾向于专注于整数查找,因此如果你'someObject[someInteger]将不会将整数转换为字符串重新查看具有整数属性的对象,例如。数组,字符串和DOM类型(NodeList s等)。

答案 1 :(得分:10)

这是我通过您的测试

得到的结果 Safari上的

  • Array.push(n)1,000,000个值:0.124 sec
  • 数组[n .. 0] =值 (降序)1,000,000个值:3.697 sec
  • 数组[0 .. n] =值(升序) 1,000,000个值:0.073秒

on FireFox:

  • Array.push(n)1,000,000个值:0.075秒
  • 数组[n .. 0] =值(降序)1,000,000个值:1.193秒
  • 数组[0 .. n] =值(升序)1,000,000个值:0.055秒
IE7上的

  • Array.push(n)1,000,000个值:2.828秒
  • 数组[n .. 0] =值(降序)1,000,000个值:1.141秒
  • 数组[0 .. n] =值(升序)1,000,000个值:7.984秒

根据你的测试 push 方法似乎在IE7上更好(差异很大),而且由于在其他浏览器上差异很小,似乎是 push 方法确实是将元素添加到数组的最佳方法。

但我创建了另一个simple test script以检查将数值附加到数组的快速方法,结果让我感到惊讶,使用Array.length的似乎比使用Array.push要快得多< / strong>,所以我真的不知道该说什么或想什么,我很无能为力。

BTW:在我的IE7上你的脚本停止了,浏览器会问我是否要让它继续下去(你知道典型的IE消息说:“停止运行这个脚本?...”) 我会重新尝试减少一些循环。

答案 2 :(得分:6)

push()是更一般[[Put]]的特例,因此可以进一步优化:

在数组对象上调用[[Put]]时,必须首先将参数转换为无符号整数,因为所有属性名称(包括数组索引)都是字符串。然后必须将其与数组的长度属性进行比较,以确定是否必须增加长度。推送时,不需要进行这样的转换或比较:只需使用当前长度作为数组索引并增加它。

当然还有其他一些会影响运行时的事情,例如调用push()应该比通过[]调用[[Put]]慢,因为原型链必须检查前者。


正如olliej指出的那样:实际的ECMAScript实现将优化转换,即对于数字属性名称,不会进行从字符串到uint的转换,只需进行简单的类型检查。基本假设应该仍然有效,尽管其影响将小于我最初的假设。

答案 3 :(得分:4)

这是一个很好的测试平台,它确认直接指派明显快于推送:http://jsperf.com/array-direct-assignment-vs-push

编辑:显示累积结果数据似乎存在一些问题,但希望很快得到解决。

答案 4 :(得分:0)

array[n] = value(在升序时)总是比array.push快,如果在前一种情况下,数组首先以长度进行初始化。

通过检查your page的javascript源代码,您的Array[0 .. n] = value (ascending)测试不会预先将数组初始化为长度。

因此,Array.push(n)有时会在第一次运行时领先,但是在随后的测试运行中,Array[0 .. n] = value (ascending)实际上始终表现最佳(在Safari和Chrome中都是如此)。

如果修改了代码,以便像var array = new Array(n)那样预先初始化一个具有长度的数组,则Array[0 .. n] = value (ascending)表示 array[n] = value比{{1 }} 的基本测试代码。

这与其他测试一致,例如@TimoKähkönen报道。具体参见他提到的测试版本:https://jsperf.com/push-method-vs-setting-via-key/10

修改后的代码,因此您可能会看到我是如何编辑它并以公平的方式初始化数组的(不必不必要地使用array.push测试用例的长度对其进行初始化):

Array.push(n)

答案 5 :(得分:-5)

Push将它添加到最后,而array [n]必须通过数组才能找到正确的位置。可能取决于浏览器及其处理数组的方式。