Node.js 中 Array、Map 和 Object 查询性能对比

几个月前给 Nest 提了一个优化 WsAdapter 寻找消息处理器性能的 PR #13378,只是简单的将遍历数组改为使用 Map。Nest 中其实存在大量通过遍历数组进行查询的代码,大多数都是启动时只执行一次,所以性能影响不大。但是 WsAdapter 中的代码是每次接收到消息都会执行,对性能有一定影响。提这个 PR 的时候,我并没有做性能测试,只是觉得 Map 的查询性能应该比遍历数组快,快多少我也不知道。直到最近有人在 PR 中问我有没有做性能测试,有没有对比过不同数据量下 Array, MapObject 的查询性能。感觉事情可能没有我想的那么简单,于是我写了一个简单的 benchmark 程序进行测试。

测试代码

javascript 复制代码
const { performance } = require("perf_hooks");

function generateKey() {
  const length = Math.floor(Math.random() * 10) + 5;
  return Math.random()
    .toString(36)
    .substring(2, length + 2);
}

function generateTestData(size) {
  const keys = new Set();
  const data = [];
  for (let i = 0; i < size; i++) {
    let key = generateKey();
    while (keys.has(key)) {
      key = generateKey();
    }
    keys.add(key);
    data.push({ key, value: `value${i}` });
  }
  return data;
}

function prepareDataStructures(data) {
  const arr = data;
  const map = new Map(data.map((item) => [item.key, item]));
  const obj = Object.fromEntries(data.map((item) => [item.key, item]));
  return { arr, map, obj };
}

function runQuery(structure, key, { arr, map, obj }) {
  switch (structure) {
    case "array":
      return arr.find((item) => item.key === key);
    case "map":
      return map.get(key);
    case "object":
      return obj[key];
  }
}

function runBenchmark(dataSize, iterations) {
  const data = generateTestData(dataSize);
  const structures = prepareDataStructures(data);

  const structureTypes = ["array", "map", "object"];
  const results = {};
  const queryKeys = Array.from(
    { length: iterations },
    () => structures.arr[Math.floor(Math.random() * dataSize)].key
  );

  structureTypes.forEach((structure) => {
    const start = performance.now();
    queryKeys.forEach((key) => runQuery(structure, key, structures));
    const end = performance.now();
    results[structure] = end - start;
  });

  console.log(`Data size: ${dataSize}, Iterations: ${iterations}`);
  structureTypes.forEach((structure) => {
    console.log(`${structure}: ${results[structure]}ms`);
  });
  console.log();
}

const dataSizes = [10, 100, 1000, 1100, 10000, 100000, 1000000];
const iterations = 10000;

dataSizes.forEach((size) => runBenchmark(size, iterations));

测试结果

测试环境:Node.js v20.10.0

yaml 复制代码
Data size: 10, Iterations: 10000
array: 4.13458251953125ms
map: 0.550999641418457ms
object: 0.47237491607666016ms

Data size: 100, Iterations: 10000
array: 2.8057079315185547ms
map: 0.23449993133544922ms
object: 0.6267919540405273ms

Data size: 1000, Iterations: 10000
array: 10.07683277130127ms
map: 0.3053750991821289ms
object: 1.0291252136230469ms

Data size: 1100, Iterations: 10000
array: 10.884374618530273ms
map: 0.2077503204345703ms
object: 0.16404247283935547ms

Data size: 10000, Iterations: 10000
array: 142.96483325958252ms
map: 1.2331666946411133ms
object: 0.2637920379638672ms

Data size: 100000, Iterations: 10000
array: 1674.5742092132568ms
map: 1.2257919311523438ms
object: 0.8130826950073242ms

Data size: 1000000, Iterations: 10000
array: 17534.821665763855ms
map: 2.955416679382324ms
object: 0.935333251953125ms

结论

可以很明显的看出在任何数据量下,MapObject 的查询性能都远远优于 Array,这并没有什么意外。但测试结果中有另一个意外的发现,MapObject 的性能差别还挺大的。在数据量小于 1000 的情况下,Map 的性能优于 Object。但在数据量大于 1000 之后,Map 的性能反而逐渐不如 Object。从测试结果中可以看出在数据量为 1100 时,MapObject 的查询性能比数据量为 1000 时反而要更好。这应该是当数据量超过 1000 后,MapObject 内部的实现都进行了优化,但 Map 的优化效果不如 Object

我们可以得出一个简单的结论,当数据量较小时使用 Map,当数据量较大时使用 Object。但这个结论并不是绝对的,可能还会受到 key 值的影响,测试中的 key 都是随机生成的长短不一的。有兴趣的话可以测一下当 key 为数字时的性能、当 key 都有重复前缀时的性能等等。

Nest WsAdapter 中的消息处理器数量一般不会太多,所以简单改为 Map 就可以提升十倍的查询性能。

私货

Nest 中的 WsAdapter 存在一个问题,它只能接收处理格式为 { event: string, data: any } 的消息,这样的消息格式对于大多数场景来说是足够的。但有时候我们无法控制消息的格式,那就只能自己继承 AbstractWsAdapter 实现寻找消息处理器的逻辑。所以我写了另一个 WsAdapter,它提供了一个自定义消息预处理器的方法,可以将消息预处理为 { event: string, data: any } 格式。并且消息处理器的查询也已经改用 Map 替代遍历数组了。有需要的可以使用看看 another-nestjs-ws-adapter,欢迎提 issue 和 PR。

相关推荐
晴子呀2 分钟前
Spring底层原理大致脉络
java·后端·spring
覆水难收呀12 分钟前
三、(JS)JS中常见的表单事件
开发语言·前端·javascript
andrew_121925 分钟前
腾讯 IEG 游戏前沿技术 二面复盘
后端·sql·面试
Lill_bin35 分钟前
JVM内部结构解析
jvm·后端·spring cloud·微服务·云原生·ribbon
evanYang_1 小时前
Spring Boot配置文件敏感信息加密
spring boot·后端·oracle
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS网上超市系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
计算机学姐2 小时前
基于python+django+vue的医院预约挂号系统
开发语言·vue.js·后端·python·mysql·django·tornado
我爱李星璇2 小时前
Spring Boot 常用注解
java·spring boot·后端
孟林洁2 小时前
xxl-job、Quartz、power-job、elastic-job对比选型
后端
ZachOn1y2 小时前
Java 入门指南:JVM(Java虚拟机)垃圾回收机制 —— 新一代垃圾回收器 ZGC 收集器
java·开发语言·jvm·后端·java-ee·个人开发