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。

相关推荐
心静财富之门14 分钟前
Flask 详细讲解 + 实战实例(零基础可学)
后端·python·flask
小陈工3 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
大鸡腿同学7 小时前
【成长类】《只有偏执狂才能生存》读书笔记:程序员的偏执型成长地图
后端
0xDevNull7 小时前
MySQL数据冷热分离详解
后端·mysql
AI袋鼠帝7 小时前
OpenClaw(龙虾)最强开源对手!Github 40K Star了,又一个爆火的Agent..
后端
午安~婉8 小时前
Electron桌面应用聊天(续)
前端·javascript·electron
哟哟耶耶9 小时前
vue3-单文件组件css功能(:deep,:slotted,:global,useCssModule,v-bind)
前端·javascript·css
是罐装可乐9 小时前
深入理解“句柄(Handle)“:从浏览器安全到文件系统访问
前端·javascript·安全
新知图书9 小时前
搭建Spring Boot开发环境
java·spring boot·后端
宸津-代码粉碎机9 小时前
Spring Boot 4.0虚拟线程实战调优技巧,最大化发挥并发优势
java·人工智能·spring boot·后端·python