JS程序员一定要知道的优化数组方法

数组是JS中使用最频繁的数据结构之一,它可以使我们存储和访问数据集。然而,有时候处理数据量较大的数据集时,我们的程序性能(这里主要指花费时间)会受到影响。本文介绍7种优化数组优化方法,减少程序花费时间。

数据量问题

我们先明确数据量的问题,当数据量不大时,程序执行主要的时间花销在CPU缓存上,此时算法的优化对程序来说不是很明显;仅当数据量大时,算法的优势才开始显著,具体概念如下(数字界限可能不准):(经过多次测试,测试例子见下文,统计结果如下)

  • 数据量<1000,有些场景下,优化算法的表现 ≤ 一般算法。
  • 数据量1000-10000,优化算法和一般算法的表现相当。
  • 数据量>10000,优化算法的优势体现。

数组优化

下面介绍几种数组的优化算法,并用不同大小数据的数据集测试优化算法的性能提升。

1. 具体数字类型数组

如果数组里只存储数组,使用具体数字类型的数组会比使用Array速度有提升,number类型的数组有:

构造函数可表示范围
Int8Array-128 ~ 127
Uint8Array0 ~ 255
Int16Array-215 ~ 215-1(32767)
Uint16Array0 ~ 216-1
Int32Array– 231 ~ 231-1
Uint32Array0 ~ 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方法也是如此。

  1. unshift需要重新排序数组,更快的方式是使用展开运算符

// unshift
arr.unshift(0)
// better way: spread operator
arr = [0,...arr]

我的尝试:
在这里插入图片描述
数据量1e4-1e6的平均耗时:
在这里插入图片描述

  • 横坐标为数量级*1e4,即1表示数量级为1e4。
  • 纵坐标为平均耗时(ms),结果表明随着数据集的增长,unshift表现比展开运算符的时间花费少,个人猜测是因为展开运算符需要数组拷贝,时间花销大。
  1. 连接数组时,使用concat来代替push

// push vs concat
arr.concat(0)
arr.push(0)

在这里插入图片描述

  • 结果表明随着数据量的增长push的代价线性增长,而concat时间花销始终保持在0.001ms级别
  1. 使用slice代替pop/shift

如果你只需要访问数组的一段元素而不需要删除这段元素,使用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慢一倍。

总结:

程序性能的影响因子:

  1. 算法,当数据量大时,算法的优劣对程序性能的影响更大
  2. 缓存,当数据量为中小程度时,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;
};

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值