将大小为10的数组合并10000次,速度为。concat为0.40 ops/sec(每秒操作数),速度为。推送速度是每秒378次。这意味着push比concat快945倍!这种差异可能不是线性的,但在这个小规模的数据量中是明显的。
在Firefox上,执行结果如下:
总的来说火狐的SpiderMonkey Java引擎比Chrome的V8引擎慢,但是。push仍然排名第一,比concat快2260倍。
我们对代码做了以上修改,修正了整个速度慢的问题。
。推送vs。concat,合并两个包含50,000个元素的数组
但是好吧,如果你合并两个50000元素的大数组而不是10000个10元素的数组呢?
以下是在Chrome上的测试结果:
。推送仍然比。concat,但这次是9次。
虽然慢了不到945倍,但已经很慢了。
更优雅的扩展操作
如果你认为数组.原型.推.
Apply(arr1,arr2)很啰嗦,可以用ES6的扩展运算符做一个简单的变换:
arr1.push(...arr2)
数组之间的性能差异。原型。用力。apply (arr1,arr2)和arr1.push(...arr2)基本上可以忽略。
但是为什么 Array.concat 这么慢? 和Java引擎有很大关系,不知道确切的答案,所以问了我的朋友@ picocreator,GPU.js的联合创始人,花了很多时间研究V8的源代码。因为我的MacBook没有足够的内存运行。concat合并了两个长度为50000的数组,@picocreator还借给了我他用来评测GPU.js运行JsPerf测试的婴儿游戏PC。
显然,答案与它们的运行机制有很大关系:合并数组时,。concat创建一个新数组,而。push只修改第一个数组。这些额外的操作(将第一个数组的元素添加到返回的数组中)是降低速度的关键。多卡。
我:“纳尼?不可能吧?就是这样而已?但为什么差距这么大?不可能啊!” @picocreator:“我可没开玩笑,试着写下 .concat 和 .push 的原生实现你就知道了!”于是我按照他说的试了试,写了几个实现,加了一个和lodash的_。concat:
本机实现模式1
让我们讨论第一组本机实现:
的本机实现。联结合并多个字符串
//创建结果数组
var arr3 = []
//add arr1
for(var I = 0;i <。arr1Lengthi++){
arr3[i] = arr1[i]
}
//add arr2
for(var I = 0;i <。arr2Lengthi++){
arr3[arr1Length + i] = arr2[i]
}
的本机实现。推
for(var I = 0;i <。arr2Lengthi++){
arr1[arr1Length + i] = arr2[i]
}
如你所见,两者唯一的区别是。push直接修改实现中的第一个数组。
常规实施方法的结果:
.concat : 75 ops/sec.push: 793 ops/sec (快 10 倍)本机实现方法1的结果:
.concat : 536 ops/sec.push : 11,104 ops/sec (快 20 倍)原来我自己写的concat和push比他们的常规实现方法要快……但是我们可以看到,简单地创建一个新数组,并将第一个数组的内容复制到其中,显然可以减缓整个过程。
本机实现2(预分配最终阵列的大小)
通过在添加元素之前预先分配数组的大小,我们可以进一步改进本机实现方法,这将产生巨大的影响。
的本机实现。连接预分配
//创建一个结果数组并预先分配其大小
var arr3 =数组(arr 1长度+arr 2长度)
//add arr1
for(var I = 0;i <。arr1Lengthi++){
arr3[i] = arr1[i]
}
//add arr2
for(var I = 0;i <。arr2Lengthi++){
arr3[arr1Length + i] = arr2[i]
}
的本机实现。预分配推送
//预先分配的大小
arr 1 . length = arr1 length+arr2 length
//将arr2的元素添加到arr1
for(var I = 0;i <。arr2Lengthi++){
arr1[arr1Length + i] = arr2[i]
}
本机实现方法1的结果:
.concat : 536 ops/sec.push : 11,104 ops/sec (快 20 倍)本地实现方法2的结果:
.concat : 1,578 ops/sec.push : 18,996 ops/sec (快 12 倍)预先分配最终数组的大小可以将每种方法的性能提高2-3倍。
。推送数组vs。推动单一元素
如果我们只是。一次推一个元素?会比array . prototype . push . apply(arr 1,arr2)快吗?
for(var I = 0;i <。arr2Lengthi++){
arr1.push(arr2[i])
}
结果
.push 整个数组:793 ops/sec.push 单个元素: 735 ops/sec (慢)所以,有道理。推送单个元素比。推送整个数组。
结论 为什么。推比。简言之,concat比。push,因为它创建了一个新数组,需要将第一个数组的元素复制到这个新数组中。现在对我来说还有另一个谜...
现在对我来说还有另一个谜...
另一个迷 为什么传统实现比本机实现慢?我再次向@picocreator求助。
我们看了罗达斯的_。concat实现,并想得到一些关于。concat,因为它们的性能相当(lodash稍快)。
事实证明,按照。concat一般实现,这个方法是重载的,支持两种传递参数的方式:
传递要添加的 n 个值作为参数,例如:[1,2].concat(3,4,5)传递要合并的数组作为参数,例如:[1,2].concat([3,4,5])甚至可以写:[1,2]。concat(3,4,[5,6])
Lodash以同样的方式重载,并支持两种传递参数的方式。Lodash将所有参数放入一个数组中,然后将其展平。所以如果你给它传递多个数组是有意义的。但是当你传递一个需要合并的数组时,它不仅会使用数组本身,还会把它复制到一个新的数组中,然后把它展平。
.....行...
所以你绝对可以优化自己的表现。这就是为什么你想自己合并数组。
另外,这只是我和@picocreator基于Lodash的源代码,他对V8源代码稍微有些过时的理解,以及他对如何常规实现。concat在引擎中工作。
可以在空空闲时间点击这里阅读lodash的源代码。
附加备注
我们的测试仅仅使用了包含整数的数组。我们都知道 Java 引擎使用规定类型的数组可以更快地执行。如果数组中有对象,结果预计会更慢。以下是用于运行基准测试的 PC 的规格: 为什么我们在 UI-licious 测试期间会进行如此大的数组操作呢?