数组是JS中使用最频繁的数据结构之一,它可以使我们存储和访问数据集。然而,有时候处理数据量较大的数据集时,我们的程序性能(这里主要指花费时间)会受到影响。本文介绍7种优化数组优化方法,减少程序花费时间。
文章目录
数据量问题
我们先明确数据量的问题,当数据量不大时,程序执行主要的时间花销在CPU缓存上,此时算法的优化对程序来说不是很明显;仅当数据量大时,算法的优势才开始显著,具体概念如下(数字界限可能不准):(经过多次测试,测试例子见下文,统计结果如下)
- 数据量<1000,有些场景下,优化算法的表现 ≤ 一般算法。
- 数据量1000-10000,优化算法和一般算法的表现相当。
- 数据量>10000,优化算法的优势体现。
数组优化
下面介绍几种数组的优化算法,并用不同大小数据的数据集测试优化算法的性能提升。
1. 具体数字类型数组
如果数组里只存储数组,使用具体数字类型的数组会比使用Array速度有提升,number类型的数组有:
构造函数 | 可表示范围 |
---|---|
Int8Array | -128 ~ 127 |
Uint8Array | 0 ~ 255 |
Int16Array | -215 ~ 215-1(32767) |
Uint16Array | 0 ~ 216-1 |
Int32Array | – 231 ~ 231-1 |
Uint32Array | 0 ~ 232-1 |
Float32Array | 1.4e-45 ~ 3.4e38 |
Float64Array | -1.7976931348623157E+308 ~ 1.7976931348623157E+308 |
普通写法与优化写法如下:
// 普通写法
var nums = [1,2,3,4,5,6,7,8,9,10]
// 优化写法
var uint8 = new Uint8Array([1,2,3,4,5,6,7,8,9,10])
实验代码:
var performances = []
var size = 10000;//100 1000 10000
var times = 50, start, end, arr,arr1; //
while(times--) {
arr = Array.apply(null, {length:size}).map((_, h) => Math.round(Math.random() * size));
start = performance.now();
arr1 = Array.apply(null, arr)
// arr1 = new Uint8Array(arr)
// arr1 = new Uint16Array(arr)
end = performance.now()
performances.push(end-start);
}
实验数据分析:
数据量为100
数据量为1000
将0.02以下的放大看:
数据量为1e4:
将0.1以下的放大看:
我们可以得出结论,
- 在数据量小于等于100时,优化前后差别不大,甚至有时优化算法的表现会稍差一点(0.03ms的差距);
- 在数据量为1000左右时,优化算法的优化效果不明显
- 当数据量大于等于10000时,优化算法的性能优势变得明显,运行速度最快可以是普通算法的2倍。
chrome
2. for循环比filter的性能更优
函数方法如map
, filter
, reduce
可以使我们更加便捷操作函数,但也可能执行降低速度。for
循环有时候会比filter
的运行速度快。
实验中,我设计了三组对照试验,分别是使用filter
, 使用for
循环和使用自定义的filter——myFilter
,操作的数据量大小在100-1e6之间,实验结果如下:
0.4以下的放大看:
实验结果表明:
- 数据量小于1e4时,for的优势还不是很明显
- 数据量在1e4-1e5之间时,for的花费时间开始明显小于filter,且自定义的myFilter的表现和for相当。
- 当数据量大于等于1e5时,for的速度是filter的两倍
实验代码:
// filter
if (compare == "filter") evens = arr.filter(fn);
// for
if (compare == "for") {
for (let i = 0; i < size; i++) {
num = arr[i];
if (num % 2 == 0) {
evens.push(num);
}
}
}
// myFilter
if (compare == "myFilter") {
evens = arr.myFilter(fn);
}
3. 减少对数组的修改
push
相比其他不修改数组本身的方法会需要更多的花销, pop
, unshift
方法也是如此。
// unshift
arr.unshift(0)
// better way: spread operator
arr = [0,...arr]
我的尝试:
数据量1e4-1e6的平均耗时:
- 横坐标为数量级*1e4,即1表示数量级为1e4。
- 纵坐标为平均耗时(ms),结果表明随着数据集的增长,unshift表现比展开运算符的时间花费少,个人猜测是因为展开运算符需要数组拷贝,时间花销大。
// push vs concat
arr.concat(0)
arr.push(0)
- 结果表明随着数据量的增长push的代价线性增长,而
concat
时间花销始终保持在0.001ms级别
如果你只需要访问数组的一段元素而不需要删除这段元素,使用slice
代替pop
/shift
// shift vs slice
arr.shift(0)
arr.slice(0,1)
- 实验表明,随着数据量的增长,
shift
的花销也随之增长;而slice
的性能稳定在0.001ms级别。
另外:
- 插入或删除数组元素时,使用展开运算符
...
代替splice
4. 并行处理数组元素
使用多核CPU时,我们可以设置对数组元素并行处理:
arr = [1,2,3,4]
arr.map(item=>item*item)
// Parallel
const squareParallel = arr.map(num => {
return new Promise(resolve => {
resolve(num * num);
});
});
Promise.all(squareParallel).then(squares => {
// squares ready
});
5. 减少数组的复制
由于复制大数组需要更过的存储空间分配和复制的过程,复制数组的速度会慢一些。
像数组的链式操作,就需要复制数组,性能更好的方式是在一次数组遍历中完成对数组元素的处理。
const arr2 = [1,2,3,4]
f = (item)=>item % 2 === 0
m = (item) => item*item
// chain
const processedChain = arr
.filter(f)
.map(m)
.slice(10)
.concat(arr2);
// traverse
const processedTraverse = [];
for (let i = 0; i < arr.length; i++) {
if (f(arr[i])) {
processedTraverse.push(m(arr[i]));
}
}
processedTraverse.push(...arr2); // Single copy
// nochain
processedNoChain = [];
processedNoChain = arr.filter(f);
processedNoChain = processedNoChain.map(m);
processedNoChain = processedNoChain.slice(0);
processedNoChain = processedNoChain.concat(arr2);
实验结果表明:
- 使用数组的链式操作略优于分步对数组操作
- 在一次遍历中完成对数组的处理会明显快于对数组进行链式操作。
不过数组的链式操作具有更强的可读性,可读性和性能需要开发者自己权衡。
- 尽量重复利用现有的数组,而不是重新创建一个数组
- 可以使用Immutable.js这种结构化存储共享库,来有效重复使用数组存储空间
6. 预先为数组分配长度
预先为数组分配适合的长度,可以减少数组扩容行为带来的花销。
不过实验表明影响并不大,个人认为Array会自动为数组分配适合的长度并在必要时扩容。
7. 初始化成本
一些数组操作有初始化成本,我们可以将数组操作的初始化成本推迟或摊销。
例如,排序方法sort
的初始化成本是:需要分析整个数组已确定排序顺序,该成本随着数组大小增长而增长。如果我们需要使用排序sort
多次,建议只一次初始化:
// Slow
const sortByName = arr => arr.sort(byName);
function process(arr) {
const arr1 = sortByName(arr);
const arr2 = sortByName(arr);
}
// Faster
let nameSorted; // Cached sort order
const sortByName = arr => {
if (!nameSorted) {
// Determine sort order once
nameSorted = computeNameSort(arr);
}
return arr.sort(nameSorted);
}
function process(arr) {
const arr1 = sortByName(arr);
const arr2 = sortByName(arr);
}
- 小插曲:本来使用vscode的插件Quokka进行实验,发现实验结果与浏览器的实验结果不一致,对比后发现Quokka慢一倍。
总结:
程序性能的影响因子:
- 算法,当数据量大时,算法的优劣对程序性能的影响更大
- 缓存,当数据量为中小程度时,CPU缓存对程序性能的影响更大。
当数组数据量大时,采用一下的优化可以使程序性能提高:
- 使用for循环,代替filter
- 减少使用
push
,shift
,pop
等会修改数组的方法 - 并行处理数组元素
- 用遍历数组代替数组的链式操作,以减少对数组的复制
- 对于排序方法
sort
,可以减少其初始化成本
参考
Speed Up JavaScript Array Processing
js生成某个范围的随机整数
完整代码
test.js
var performances = new Map();
var size = 1e7; //100 1000 10000
var mul = 1000;
var scales = generate_array(50, null, true, mul);
// var scales = [100, 1000, 10000, 100000];
// console.log("scales", scales);
var times = 50,
start,
end,
arr,
arr1; //
const comparations = ["normal", "typed"];
const f = (item) => item % 2 === 0;
const m = (item) => item * item;
const arr2 = [1, 2, 3, 4];
var processedChain = [];
var processedTravers = [];
var processedNoChain = [];
// const fn = (num) => num % 2 === 0;
scales.forEach((scale) => {
var size = scale;
comparations.forEach((compare) => {
var setName = compare + "-" + scale;
performances.set(setName, []);
var t = times;
while (t--) {
arr = generate_array(size);
var evens = [];
var num;
start = performance.now();
// normal
if (compare == "normal") {
var arr1 = Array.apply(null, arr);
}
// typed
if (compare == "typed") {
var arr1 = new Uint16Array(arr);
}
// filter
if (compare == "filter") evens = arr.filter(fn);
// for
if (compare == "for") {
for (let i = 0; i < size; i++) {
num = arr[i];
if (num % 2 == 0) {
evens.push(num);
}
}
}
// myFilter
if (compare == "myFilter") {
evens = arr.myFilter(fn);
}
// unshift
if (compare == "unshift") {
arr.unshift(0);
}
if (compare == "spread...") {
arr = [0, ...arr];
}
if (compare == "shift") {
arr.concat(0);
}
if (compare == "push") {
arr.push(0);
}
if (compare == "serial") {
var newArr1 = arr.map((item) => item * item);
}
if (compare == "parallel") {
const doubleParallel = arr.map((num) => {
return new Promise((resolve) => {
resolve(num * num);
});
});
// var newArr2 = await Promise.all(doubleParallel);
}
if (compare == "no_chain") {
processedNoChain = [];
processedNoChain = arr.filter(f);
processedNoChain = processedNoChain.map(m);
processedNoChain = processedNoChain.slice(0);
processedNoChain = processedNoChain.concat(arr2);
}
if (compare == "chain") {
processedChain = [];
processedChain = arr.filter(f).map(m).slice(10).concat(arr2);
}
if (compare == "traverse") {
processedTravers = [];
for (let i = 0; i < arr.length; i++) {
if (f(arr[i])) {
var afterMap = m(arr[i]);
processedTravers.push(afterMap);
}
}
processedTravers.push(...arr2); // Single copy
}
// if (compare == "normal") {
// var newArr = [];
// arr.forEach((item) => {
// if (f(item)) {
// newArr.push(item);
// }
// });
// }
if (compare == "pre_allocate") {
var newArr = Array(null, { length: size });
arr.forEach((item) => {
if (f(item)) {
newArr.push(item);
}
});
}
end = performance.now();
// console.log("setName", setName);
performances.get(setName).push(end - start);
}
});
// visualize("scatter", performances, 100, "性能随数据量变化对比");
});
var categoryPerformance = new Map();
// console.log("performances", performances);
performances.forEach((value, key) => {
// console.log("value", value);
var newKey = key.split("-")[0];
var index = key.split("-")[1] / mul;
!categoryPerformance.get(newKey) && categoryPerformance.set(newKey, []);
categoryPerformance.get(newKey)[index] = value.avg();
});
visualize("line", categoryPerformance, 50, "性能随数据量变化对比");
tools.js
var myChart = echarts.init(document.getElementById("chatrs"));
function visualize(type, performance, len, title = "标题") {
var data = [];
var legendName = [];
performance.forEach((value, key) => {
legendName.push(key);
data.push({
name: key,
type: type,
data: value,
});
});
console.log("Visualization", performance, data, legendName, type);
var option = {
title: { text: title },
legend: {
data: legendName,
},
xAxis: {
data: generate_array(len, null, true),
name: "数据量 / *5e3",
},
yAxis: { name: "耗时 / ms" },
series: data,
};
myChart.setOption(option);
}
function generate_array(size, fill, continously, multiple = 1) {
const ret = Array(size);
for (let i = 1; i <= size; i++) {
ret[i - 1] = fill ? multiple * fill : continously ? i * multiple : Math.floor(Math.random() * 1000);
}
return ret;
}
// myFilter
Array.prototype.myFilter = function (fn) {
//fn作为判断元素的条件,所以返回值为boolean
var newArray = [];
var len = this.length;
for (var i = 0; i < len; i++) {
//判断是否满足函数条件
if (fn(this[i], i)) {
//过滤出满足条件的元素并深度克隆下来。
if (typeof this[i] == "object") {
var obj = {};
newArray.push(deepClone(obj, this[i]));
} else {
newArray.push(this[i]);
}
}
}
return newArray;
};
Array.prototype.avg = function (call) {
let type = Object.prototype.toString.call(call);
let sum = 0;
let max = Math.max(this);
let min = Math.min(this);
let maxIndex = this.findIndex((item) => item == max);
let minIndex = this.findIndex((item) => item == min);
this.splice(maxIndex, 1);
this.splice(minIndex, 1);
if (type === "[object Function]") {
sum = this.reduce((pre, cur, i) => pre + call(cur, i), 0);
} else {
sum = this.reduce((pre, cur) => pre + cur);
}
return sum / this.length;
};