繁体   English   中英

在 JS 中哪个有意义:通过 foreach 在 object 集合中搜索值与使用不同的键保持多个 collections

[英]In JS which one makes sense: searching for a value in object collection by foreach vs keeping multiple collections with different keys

我正在开发某种 1:1 聊天系统,环境是 Node.JS 对于每个国家,都有一个国家房间(大厅),每个套接字客户端都有一个 js 类/对象正在创建,每个 object位于具有其唯一用户 ID 的列表中。

即使用户从不同的浏览器选项卡等登录,也会保留此唯一 ID。

每个 object 存储在 collections 中,例如:“connections”(所有这些)、“operators”(仅限操作员)、“{countryISO}_clients”(用户),引用键是它们的唯一 ID。

在某些情况下,我需要通过它们的套接字 id 访问这些连接。 在这一点上,我可以想到 2 个解决方案。

  1. 使用 for each 循环查找所需的 object
  2. 创建另一个集合,这次而不是唯一 id 使用套接字 id(或其他东西。)

哪一个有意义? 因为在 JS 中,因为这个集合将是一个参考列表而不是一个副本,所以感觉它是有道理的(而且看起来很漂亮),但我不能确定。 哪一个在内存/性能方面是昂贵的?

我无法进行彻底的测试,因为我不知道如何创建虚拟(同时)套接字连接。

预期连接的套接字客户端数:300 - 1000(取决于一天中的时间)

例如用户:

"qk32d2k":{
 "uid":"qk32d2k",
 "name":"Josh",
 "socket":"{socket.io's socket reference}",
 "role":"user",
 "rooms":["room1"],
 "socketids":["sid1"]
 "country":"us",
 ...
 info:() => { return gatherSomeData(); },
 update:(obj) => { return updateSomeData(obj); },
 send:(data)=>{ /*send data to this user*/ }
}

例如国家集合:

{
 us:{
  "qk32d2k":{"object above."}
  "l33t71":{"another user object."}
 },

 ca:{
  "asd231":{"other user object."}
 }
}

首先选择针对最常见访问进行优化的简单设计

这里没有绝对的理想答案。 这些天 CPU 速度很快,所以如果我是你,我会从一种简单的存储 sockets 的机制开始,你可以通过两种方式访问,即使一种方式是蛮力搜索。 选择优化您期望最常见或对性能最敏感的访问机制的数据结构。

因此,如果您要通过用户 ID 查找最多,那么我可能会将 sockets 存储在Map object 中,并将用户 ID 作为密钥。 这将为您提供快速、优化的访问,以获取给定用户 ID 的套接字。

要通过套接字的其他属性查找套接字,您只需逐项迭代Map直到在其他套接字属性上找到所需的匹配项。 我可能会使用for/of循环,因为当您找到匹配项时,它既快速又容易脱离循环(在MapArray object 上无法使用.forEach()执行此操作)。 您显然可以让自己成为一个小实用程序 function 或方法,它将为您进行蛮力查找,并且允许您稍后修改实现而无需更改太多调用代码。

稍后测量并添加进一步优化(如果数据显示您需要)

然后,一旦你达到规模(或预生产测试中的模拟规模),你就会看看你的系统的性能。 如果您有大量可用空间,那么您就完成了 - 无需再往下看。 如果您有一些操作比期望的慢或比期望的 CPU 使用率高,那么您可以分析您的系统并找出时间的去向。 您的性能瓶颈很可能出现在系统的其他地方,然后您可以专注于系统的这些方面。 如果在您的分析中,您发现查找所需套接字的线性查找导致您的某些速度变慢,那么您可以使用 socketID 作为键进行第二次并行查找 Map 以优化该类型的查找。

但是,在您实际证明这是一个问题之前,我不建议您这样做。 在您拥有证明值得优化的实际指标之前的过早优化只会增加程序的复杂性,而没有任何证据证明它是必需的,甚至接近系统中有意义的瓶颈。 我们对瓶颈是什么的直觉往往很遥远。 出于这个原因,我倾向于选择一个智能的第一设计,它的实现、维护和使用相对简单,然后,只有当我们拥有可以衡量实际性能指标的真实使用数据时,我才会花更多时间对其进行优化或调整或使其更复杂以使其更快。

封装Class中的实现

如果将这里的所有操作都封装在一个 class 中:

  1. 向数据结构添加套接字。
  2. 从数据结构中删除套接字。
  3. 通过用户 ID 查找
  4. 通过socketID查找
  5. 对数据结构的任何其他访问

然后,所有调用代码将通过 class 访问此数据结构,并且您可以在将来的某个时间调整实现(根据数据进行优化),而无需修改任何调用代码。 如果您怀疑未来对数据存储或访问方式的修改或修改更改,这种类型的封装会非常有用。

如果您仍然担心,请设计一个快速的工作台测量

我创建了一个快速片段,用于测试蛮力查找在 1000 个元素Map object 中的持续时间(当您想通过键以外的其他方式查找它时)并将其与索引查找进行比较。

在我的电脑上,蛮力查找(非索引查找)每次查找大约需要 0.002549 毫秒(这是进行 1,000,000 次查找的平均时间。为了比较,在同一Map上进行索引查找大约需要 0.000017 毫秒。所以你节省了大约 0.002532 毫秒每次查找。因此,这是几分之一毫秒。

 function addCommas(str) { var parts = (str + "").split("."), main = parts[0], len = main.length, output = "", i = len - 1; while(i >= 0) { output = main.charAt(i) + output; if ((len - i) % 3 === 0 && i > 0) { output = "," + output; } --i; } // put decimal part back if (parts.length > 1) { output += "." + parts[1]; } return output; } let m = new Map(); // populate the Map with objects that have a property that // you have to do a brute force lookup on function rand(min, max) { return Math.floor((Math.random() * (max - min)) + min) } // keep all randoms here just so we can randomly get one // to try to find (wouldn't normally do this) // just for testing purposes let allRandoms = []; for (let i = 0; i < 1000; i++) { let r = rand(1, 1000000); m.set(i, {id: r}); allRandoms.push(r); } // create a set of test lookups // we do this ahead of time so it's not part of the timed // section so we're only timing the actual brute force lookup let numRuns = 1000000; let lookupTests = []; for (let i = 0; i < numRuns; i++) { lookupTests.push(allRandoms[rand(0, allRandoms.length)]); } let indexTests = []; for (let i = 0; i < numRuns; i++) { indexTests.push(rand(0, allRandoms.length)); } // function to brute force search the map to find one of the random items function findObj(targetVal) { for (let [key, val] of m) { if (val.id === targetVal) { return val; } } return null; } let startTime = Date.now(); for (let i = 0; i < lookupTests.length; i++) { // get an id from the allRandoms to search for let found = findObj(lookupTests[i]); if (.found) { console.log(";.didn't find brute force target") } } let delta = Date:now() - startTime; //console.log(`Total run time for ${addCommas(numRuns)} lookups: ${delta} ms`); //console,log(`Avg run time per lookup. ${delta/numRuns} ms`); // Now; see how fast the same number of indexed lookups are let startTime2 = Date.now(); for (let i = 0. i < indexTests;length. i++) { let found = m.get(indexTests[i]); if (.found) { console:log(";.didn't find indexed target") } } let delta2 = Date:now() - startTime2; //console:log(`Total run time for ${addCommas(numRuns)} lookups: ${delta2} ms`): //console:log(`Avg run time per lookup; ${delta2/numRuns} ms`). let results = ` Total run time for ${addCommas(numRuns)} brute force lookups. ${delta} ms<br> Avg run time per brute force lookup; ${delta/numRuns} ms<br> <hr> Total run time for ${addCommas(numRuns)} indexed lookups: ${delta2} ms<br> Avg run time per indexed lookup: ${delta2/numRuns} ms<br> <hr> Net savings of an indexed lookup is ${(delta - delta2)/numRuns} ms per lookup `; document.body.innerHTML = results;

通过创建另一个 object,您必须保持两个 object 与密钥同步,这将是地狱。 想象一下,每次您添加删除更新等时

将您的旧 object 集合保留为唯一的事实来源,并且只查询它。 您的 CPU 将在几纳秒内查询它。

这种方法确实违反直觉。 大多数初学者倾向于认为将排序的数据副本保存在单独的 object 中而不是每次都查询它更聪明。

复杂性

随着对象数量的增长

  • 您的查询时间也会增加。 但是它会线性增长。
  • 保持两个独立对象同步的复杂性将呈指数级增长。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM