作为测试某些代码的附带结果,我编写了一个小函数来比较使用array.push方法与直接寻址(array [n] = value)的速度。令我惊讶的是,推送方法通常显示出更快的速度,尤其是在Firefox中,有时在Chrome中。出于好奇:有人对此有解释吗?您可以在此页面上找到测试(单击“数组方法比较”)
作为测试某些代码的附带结果,我编写了一个小函数来比较使用array.push方法与直接寻址(array [n] = value)的速度。令我惊讶的是,推送方法通常显示出更快的速度,尤其是在Firefox中,有时在Chrome中。出于好奇:有人对此有解释吗?您可以在此页面上找到测试(单击“数组方法比较”)
Answers:
各种各样的因素都起作用,大多数JS实现使用平面数组,如果以后需要的话,可以转换为稀疏存储。
基本上,决定稀疏的决定是基于设置哪些元素以及要浪费多少空间才能保持平坦的启发式方法。
在您的情况下,您将首先设置最后一个元素,这意味着JS引擎将看到一个数组,该数组需要具有一个长度,n
但只有一个元素的长度。如果n
足够大,则将立即使该数组成为稀疏数组-在大多数引擎中,这意味着所有后续插入将采用慢速稀疏数组的情况。
您应该添加一个额外的测试,在其中填充从索引0到索引n-1的数组-它应该快得多。
为了响应@Christoph并出于拖延的愿望,这里描述了如何在JS中(通常)实现数组-具体情况因JS引擎而异,但一般原理是相同的。
所有JS Object
(而不是字符串,数字,true,falseundefined
或null
)都不继承自基本对象类型-确切的实现有所不同,可能是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类型(NodeList
s等)。
dslot
成员(d为动态成员),只要JS数组密集,该成员将保存一个实际数组;我没有检查稀疏数组或使用非数组索引属性名称时会发生什么情况
这是我通过测试得到的结果
在Safari上:
在FireFox上:
在IE7上:
根据您的测试,push方法似乎在IE7上更好(差异很大),并且由于在其他浏览器上的差异很小,因此看来push方法确实是将元素添加到数组的最佳方法。
但是我创建了另一个简单的测试脚本来检查哪种快速将值添加到数组的方法,结果让我感到非常惊讶,与使用Array.push相比,使用Array.length似乎要快得多,所以我真的不知道再说或思考,我一无所知。
顺便说一句:在我的IE7上,您的脚本停止了,浏览器问我是否要继续进行(您知道典型的IE消息说:“停止运行此脚本吗?...”),我会建议减少一些循环。
push()
是更通用的[[Put]]的特例,因此可以进一步优化:
在数组对象上调用[[Put]]时,必须首先将参数转换为无符号整数,因为所有属性名称(包括数组索引)都是字符串。然后必须将其与数组的length属性进行比较,以确定是否必须增加长度。推入时,无需进行此类转换或比较:只需使用当前长度作为数组索引并增加它。
当然,还有其他一些因素会影响运行时间,例如,调用push()
应比通过[[Put]]的调用慢,[]
因为必须检查原型链的前者。
正如olliej指出的那样:实际的ECMAScript实现将优化转换,例如,对于数字属性名称,不执行从字符串到uint的转换,而只是进行简单的类型检查。尽管其影响将小于我最初的假设,但基本假设仍应成立。
这是一个很好的测试平台,它可以确认直接分配比推式要快得多:http : //jsperf.com/array-direct-assignment-vs-push。
编辑:显示累积结果数据似乎有些问题,但希望它很快得到解决。
push
然后在测试中,使用添加另外1,000个元素push
。通过在您的第一个测试中简单地更改new Array(len)
为[]
,我看到的结果要接近得多,并且实际上建议push
从空数组中使用它会稍微快一些。jsbin.com/epesed/22
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;
}