作为测试一些代码的副作用,我写了一个小函数来比较使用array.push方法和直接寻址(array [n] = value)的速度。令我惊讶的是,推送方法通常表现得更快,特别是在Firefox中,有时在Chrome中。只是出于好奇:任何人都有解释吗? 您可以找到测试@ this page(单击“数组方法比较”)
答案 0 :(得分:80)
各种各样的因素都会发挥作用,大多数JS实现都会使用一个平面数组,如果以后需要它就会转换为稀疏存储。
基本上,变稀疏的决定是基于正在设置什么元素的启发式算法,以及为了保持平坦而浪费多少空间。
在您的情况下,您首先设置最后一个元素,这意味着JS引擎将看到一个数组需要长度为n
但只有一个元素。如果n
足够大,这将立即使数组成为稀疏数组 - 在大多数引擎中,这意味着所有后续插入都将采用慢稀疏数组的情况。
你应该添加一个额外的测试,在其中你将数组从索引0填充到索引n-1 - 它应该更快,更快。
为了回应@Christoph并且出于拖延的愿望,这里描述了如何(通常)在JS中实现数组 - 具体内容从JS引擎到JS引擎不等,但一般原理是相同的。
所有JS Object
(所以不是字符串,数字,true,false,undefined
或null
)都从基础对象类型继承 - 确切的实现方式各不相同,它可以是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上的:
on FireFox:
:
根据你的测试, 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]必须通过数组才能找到正确的位置。可能取决于浏览器及其处理数组的方式。