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


73

作为测试某些代码的附带结果,我编写了一个小函数来比较使用array.push方法与直接寻址(array [n] = value)的速度。令我惊讶的是,推送方法通常显示出更快的速度,尤其是在Firefox中,有时在Chrome中。出于好奇:有人对此有解释吗?您可以在此页面上找到测试(单击“数组方法比较”)


如果IE6已充分更新,则应支持。据我所知,在IE 5.5左右的某个地方,出现了一个新的jscript引擎,该引擎支持推(在此之前,我使用了Home Brew Array增强)。
KooiInc'3

当然,您可以将push添加到ie6数组中-但这可能会实现为push(value){this [this.length] = value}函数,因此您将测试同一件事
olliej,2009年

5
IE6将始终至少具有JScript 5.6。只是IE 5.0的基本JScript实现不支持Array.push();。其他人都在原始JavaScript 1.2中重新获得了它。
bobince

Answers:


87

各种各样的因素都起作用,大多数JS实现使用平面数组,如果以后需要的话,可以转换为稀疏存储。

基本上,决定稀疏的决定是基于设置哪些元素以及要浪费多少空间才能保持平坦的启发式方法。

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

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

为了响应@Christoph并出于拖延的愿望,这里描述了如何在JS中(通常)实现数组-具体情况因JS引擎而异,但一般原理是相同的。

所有JS Object(而不是字符串,数字,true,falseundefinednull)都不继承自基本对象类型-确切的实现有所不同,可能是C ++继承,也可能是在C中手动实现(以任何一种方式这样做都是有好处的)-基本的Object类型定义默认的属性访问方法,例如

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]]逻辑。否则,将值放入数组自己的存储中,如果数据足够紧凑,则引擎将使用平面数组存储,在这种情况下,插入(和检索)仅是标准数组索引操作,否则引擎将转换数组稀疏存储,并放置/获取使用映射来从propertyName到值位置。

老实说,我不确定在发生这种转换后,当前是否有任何JS引擎从稀疏存储转换为平面存储。

顺便说一句,这是对所发生情况的一个较高层次的概述,并省略了一些更棘手的细节,但这是一般的实现模式。各个引擎之间如何存储额外的存储以及如何分配/放置的细节各不相同-但这是我可以真正描述设计/实现的最清晰的信息。

一个较小的附加点,虽然ES规范指的propertyName是字符串JS引擎也倾向于专门处理整数查找,所以someObject[someInteger]如果您查看的是具有整数属性的对象,则不会将整数转换为字符串。数组,字符串和DOM类型(NodeLists等)。


1
@olliej:“大多数JS实现使用平面数组,如果以后需要的话,可以转换为稀疏存储”-很有意思。那么数组对象有两种存储方式:一种用于常规属性,一种用于数组条目?
Christoph

@Christoph:是的-如果您愿意的话,我可以详细介绍,但会偏向于JavaScriptCore / Nitro的实现-SpiderMonkey,V8和KJS中的通用模型相同,但我不知道他们的确切实施细节
olliej

@olliej:刚刚检查了SpiderMonkey的源代码:JSObject结构体包含一个dslot成员(d为动态成员),只要JS数组密集,该成员将保存一个实际数组;我没有检查稀疏数组或使用非数组索引属性名称时会发生什么情况
Christoph

@olliej:谢谢,这很有道理。我在页面上添加了[0..n]测试,速度更快,而且我理解原因。与推[0..n]相比,在所有浏览器中都更快。
KooiInc,2009年

@Christoph:是的,它们是我在(过长)带注释的答案中提到的C样式的实现;JSC,V8和KJS都是C ++表示,JSC和V8将属性哈希表与对象分开存储,iirc SM使用树的哈希表而不是哈希表-每个人做不同的事情
olliej

11

这是我通过测试得到的结果

在Safari上:

  • Array.push(n)1,000,000值:0.124秒
  • Array [n .. 0] =值(递减)1,000,000值:3.697秒
  • Array [0 .. n] =值(升序)1,000,000值:0.073秒

在FireFox上:

  • Array.push(n)1,000,000值:0.075秒
  • Array [n .. 0] =值(递减)1,000,000值:1.193秒
  • Array [0 .. n] =值(升序)1,000,000值:0.055秒

在IE7上:

  • Array.push(n)1,000,000值:2.828秒
  • Array [n .. 0] =值(递减)1,000,000值:1.141秒
  • Array [0 .. n] =值(递增)1,000,000值:7.984秒

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

但是我创建了另一个简单的测试脚本来检查哪种快速将值添加到数组的方法,结果让我感到非常惊讶,与使用Array.push相比使用Array.length似乎要快得多,所以我真的不知道再说或思考,我一无所知。

顺便说一句:在我的IE7上,您的脚本停止了,浏览器问我是否要继续进行(您知道典型的IE消息说:“停止运行此脚本吗?...”),我会建议减少一些循环。


6

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

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

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


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


2
所有的JS引擎实际上都假设整数是优化[[Put]]的,前提是如果您使用的是整数,则该类型可能是具有用于Integer属性名称的特殊处理程序的类型-例如。数组,字符串以及DOM类型(NodeList,CanvasPixelArray等)
olliej

1
错误,完成最后一条注释-他们首先假定Integer,然后通用对象回退会将Integer转换为字符串,然后尝试使用字符串表示形式。
olliej

6

这是一个很好的测试平台,它可以确认直接分配比推式要快得多:http : //jsperf.com/array-direct-assignment-vs-push

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


4
您的测试存在严重缺陷。在这两个测试中,您要预先分配每个包含1,000个元素的数组。push然后在测试中,使用添加另外1,000个元素push。通过在您的第一个测试中简单地更改new Array(len)[],我看到的结果要接近得多,并且实际上建议push从空数组中使用它会稍微快一些jsbin.com/epesed/22
丹涛

感谢您的评论!是的,你是对的。最慢的部分是创建数组,而不是推入。我更新了答案。
TimoKähkönen2013年

4
为什么要添加注释“请忽略下面的测量表。请参见编辑2。”?为什么不删除我们应该忽略的表呢?如您所写,您的答案非常令人困惑。没人关心编辑,他们关心书面答复。如果人们确实关心编辑历史记录,那么他们就可以使用它。
Bryan Oakley

我同意,这是一个令人困惑的答案。该表对我来说是进行新测量的基础。
TimoKähkönen2013年

我找到了一个jsperf,并用它替换了令人困惑的表。
TimoKähkönen2013年

0

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

从检查的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进行4.5倍至9x的速度比Array.push(n)在我的这个具体的测试代码初步运行。

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

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

function testArr(n, doPush){

  var now = new Date().getTime(),
                  duration,
                  report =  ['<b>.push(n)</b>',
                             '<b>.splice(0,0,n)</b>',
                             '<b>.splice(n-1,0,n)</b>',
                             '<b>[0 .. n] = value</b> (ascending)',
                             '<b>[n .. 0] = value</b> (descending)'];
  doPush = doPush || 5;

  if (doPush === 1) {
   var arr = [];
   while (--n) {
     arr.push(n);
   }
  } else if (doPush === 2) {
   var arr = [];
   while (--n) {
    arr.splice(0,0,n);
   }
  } else if (doPush === 3) {
   var arr = [];
   while (--n) {
    arr.splice(n-1,0,n);
   }
  } else if (doPush === 4) {
   var arr = new Array(n);
   for (var i = 0;i<n;i++) {
    arr[i] = i;
   }
  } else {
    while (--n) {
    var arr = [];
      arr[n] = n;
    }
  }
  /*console.log(report[doPush-1] + '...'+ arr.length || 'nopes');*/
  duration = ((new Date().getTime() - now)/1000);
  $('zebradinges').innerHTML +=  '<br>Array'+report[doPush-1]+' 1.000.000 values: '+duration+' sec' ;
  arr = null;
}

-6

Push将其添加到末尾,而array [n]必须遍历数组以找到正确的位置。可能取决于浏览器及其处理数组的方式。


2
在测试的情况下,n是已知的(等效于[array] .length-1),因此不会进行搜索。
KooiInc

如果要查找第n个元素,则需要在数组中找到指向该点的指针以填充值。
Stiropor

在测试的情况下,n是已知的。但是,编写的Javascript库完全不了解您的测试,即使您非常清楚它在哪里,也可能仍然[]在数组中搜索正确的位置。考虑一下带有尾指针的链表。
Chuck
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.