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

Why is array.push sometimes faster than array[n] = value?

作为测试一些代码的附带结果,我编写了一个小函数来比较使用array.push方法与直接寻址(array[n]=value)的速度。令我惊讶的是,推送方法经常表现得更快,特别是在火狐和Chrome中。只是出于好奇:有人对此有解释吗?您可以在本页找到测试(单击"数组方法比较")。


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

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

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

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

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

所有的JS EDCOX1,2,s(所以不串,数字,真,假,EDOCX1,3,或EDOCX1,4)从基类对象类型继承——确切的实现是不同的,它可以是C++继承,或者是在C中手工操作(无论用哪种方式都有好处)——基对象类型定义默认值属性访问方法,例如

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

此对象类型处理所有标准属性访问逻辑、原型链等。然后数组实现变成

1
2
3
4
5
6
7
8
9
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中创建数组时,引擎将创建类似于上述数据结构的内容。在数组实例中插入对象时,数组的Put方法会检查属性名是否是0到2^32-1(或者可能是2^31-1,我完全忘记)之间的整数(或者可以转换为整数,例如"121"、"2341"等)。如果不是,则将put方法转发到基本对象实现,并完成标准的[[put]]逻辑。否则,将该值放入数组自己的存储中,如果数据足够紧凑,则引擎将使用平面数组存储,在这种情况下,插入(和检索)只是一个标准的数组索引操作,否则引擎将把数组转换为稀疏存储,并将put/get使用映射从propertyname获取值位置。

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

总之,这是对所发生的事情的一个相当高级别的概述,并且遗漏了一些更棘手的细节,但这是一般的实现模式。附加存储和Put/Get如何发送的细节在不同的引擎之间是不同的——但这是最清楚的,我可以真正描述设计/实现。

一个小的附加点,而es规范引用propertyName作为字符串JS引擎也倾向于专门进行整数查找,因此如果您查看的对象具有整数属性,例如数组、字符串和dom类型(NodeList),someObject[someInteger]将不会将整数转换为字符串等)


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

狩猎之旅:

  • array.push(n)1000000值:0.124秒
  • 数组…0=值(降序)1000000个值:3.697秒
  • 数组[ 0…n]=值(升序)1000000值:0.073秒

火狐:

  • array.push(n)1000000值:0.075秒
  • 数组…0]=值(降序)1000000值:1.193秒
  • 数组[ 0…n]=值(升序)1000000值:0.055秒

论基督论:

  • array.push(n)1000000值:2.828秒
  • 数组…0]=值(降序)1000000值:1.141秒
  • 数组[ 0…n]=值(升序)1000000值:7.984秒

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

但是我创建了另一个简单的测试脚本来检查什么方法可以快速地将值附加到数组中,结果让我很惊讶,与使用array.push相比,使用array.length似乎要快得多,所以我真的不知道该说什么或该怎么想了,我一无所知。

顺便问一句:在我的IE7上,你的脚本停止运行,浏览器问我是否要继续运行(你知道典型的IE消息说:"停止运行这个脚本吗?"……")我会建议减少一点环。


push()是更一般的一种特殊情况,因此可以进一步优化:

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

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

正如Olliej指出的那样:实际的EcmaScript实现将优化转换,即对于数字属性名,不执行从字符串到uint的转换,只执行简单的类型检查。基本假设应该仍然成立,尽管它的影响将比我最初假设的要小。


这里有一个很好的测试台,它证实了直接分配比push快得多:http://jspef.com/array-direct-assignment-vs-push。

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


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