作为测试一些代码的附带结果,我编写了一个小函数来比较使用 array.push 方法与直接寻址 (array[n] = value) 的速度。令我惊讶的是,推送方法通常显示出更快,尤其是在 Firefox 中,有时在 Chrome 中。只是出于好奇:有人对此有解释吗?您可以在@此页面找到测试(单击“数组方法比较”)
为什么 array.push 有时比 array[n] = value 快?
各种因素都会起作用,大多数 JS 实现都使用平面数组,如果以后需要,它会转换为稀疏存储。
基本上,变得稀疏的决定是基于正在设置的元素以及为了保持平坦而浪费多少空间的启发式方法。
在您的情况下,您首先设置最后一个元素,这意味着 JS 引擎将看到一个数组,该数组的长度n
只需要一个元素。如果n
足够大,这将立即使数组成为稀疏数组——在大多数引擎中,这意味着所有后续插入都将采用慢速稀疏数组的情况。
您应该添加一个额外的测试,在其中填充从索引 0 到索引 n-1 的数组——它应该快得多。
为了回应@Christoph 并出于拖延的愿望,这里描述了数组是如何(通常)在 JS 中实现的——具体情况因 JS 引擎而异,但一般原则是相同的。
所有 JS Object
(不是字符串、数字、真、假、undefined
或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 引擎当前从稀疏存储转换为平面存储。
Anyhoo,这是对发生的事情的相当高层次的概述,并遗漏了一些更棘手的细节,但这是一般的实现模式。附加存储的具体细节以及 put/get 的调度方式因引擎而异——但这是我能真正描述设计/实现的最清楚的地方。
一个小的补充点,虽然 ES 规范指的propertyName
是字符串 JS 引擎也倾向于专门用于整数查找,因此someObject[someInteger]
如果您正在查看具有整数属性的对象,则不会将整数转换为字符串,例如。数组、字符串和 DOM 类型(NodeList
s 等)。
这些是我通过你的测试得到的结果
在 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 秒
在火狐上:
- 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.length 似乎比使用 Array.push 快得多,所以我真的不知道是什么再说或想了,我一无所知。
顺便说一句:在我的 IE7 上,你的脚本停止了,浏览器问我是否想让它继续(你知道典型的 IE 消息说:“停止运行这个脚本?......”)我会建议减少一点循环.
push()
是更一般的 [[Put]] 的特例,因此可以进一步优化:
在数组对象上调用 [[Put]] 时,必须首先将参数转换为无符号整数,因为所有属性名称 - 包括数组索引 - 都是字符串。然后必须将它与数组的长度属性进行比较,以确定是否必须增加长度。推送时,无需进行此类转换或比较:只需使用当前长度作为数组索引并增加它。
当然还有其他一些会影响运行时的事情,例如调用push()
应该比调用 [[Put]] via 慢,[]
因为原型链必须检查前者。
正如 olliej 指出的那样:实际的 ECMAScript 实现将优化转换,即对于数字属性名称,没有完成从字符串到 uint 的转换,而只是简单的类型检查。基本假设应该仍然成立,尽管它的影响将比我最初假设的要小。
这是一个很好的测试平台,它证实直接分配比推送快得多:http : //jsperf.com/array-direct-assignment-vs-push。
编辑:显示累积结果数据似乎存在一些问题,但希望它很快得到修复。
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)
thenArray[0 .. n] = value (ascending)
显示的那样,它的array[n] = value
执行速度比Array.push(n)
我对这个特定测试代码的基本运行快 4.5 到 9 倍。
这与其他测试一致,如@Timo Kä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;
}