【问题标题】:Do Javascript Maps have a set amount of keys that they can set or do they have a set amount of key operations that can be done?Javascript Maps 是否有一定数量的可以设置的键,或者它们是否有一定数量的可以完成的键操作?
【发布时间】:2020-07-30 17:22:15
【问题描述】:

所以我知道 Javascript 地图有一定数量的可以存储的键(大约 1670 万)。

我试图测试我是否可以(以一种非常丑陋的方式)从数组中删除最旧的元素。我注意到,无论我做什么,实际上并不是地图大小是一个限制因素,而是我所做的操作数量限制了我。

下面是示例代码:

const map = new Map();
let i = 0;

while (true) {
  i++;

  set(i, i);

  if (i % 1000 === 0)
    console.log('INSERTED: ', i, 'KEYS', 'MAP SIZE :', map.size);
}

function set(key, value) {
  if (map.size > 16770000) {
    Array.from(map.keys()).slice(0, 10000).forEach(key => map.delete(key));
    console.log('DELETED, current map size:', map.size);
  }

  try {
    map.set(key, value);
  } catch (e) {
    console.log('MAP SIZE:', map.size, 'INSERTED:', key);
    throw e;
  }
}

当您运行 sn-p 时,只需检查您的控制台。您应该注意到最后(抛出异常时)您将获得 Map Size 和 INSERTED。 Map Size 将是一个变量(取决于您删除的元素数量,在本例中为 10000),但 INSERTED 将始终是相同的值。那么,如果我没有达到地图的极限怎么办......我以某种方式达到了极限。这是我缺少的某种参考问题吗?

编辑:正如@CRice 所提到的,如果您将删除的项目增加到大约 10,000,000 个,那么这个循环似乎会永远持续下去。

编辑 2:这是来自 V8 开发人员之一的回答,谈到 16.7M 键的限制:https://stackoverflow.com/a/54466812/5507414

编辑 3:见答案:https://stackoverflow.com/a/63234302/5507414。我们仍然需要 V8 开发人员或对引擎有进一步了解的人来澄清这一点。

【问题讨论】:

  • 不确定我是否理解这个问题,你能澄清一下吗?我在控制台中看到最后地图的大小为 16767216(如您所说,约为 16.7M)。最后插入的密钥是16777217,比最大地图大小高 10000,日志显示您在此之前删除了 10000 个密钥,所以对我来说一切都很好......
  • 好吧,如果我删除了 10000 个键,那是不是意味着我应该可以再设置 10000 个键?问题是我不能。插入的最后一个键实际上应该多 10000 个。但事实并非如此。如果你得到 sn-p 并把它改成不是 10000 而是 20000 你会看到最后插入的键仍然是 16777217。为什么我删除键时没有更多空间?
  • 是的,我现在明白你的意思了。这很好奇。我还注意到,如果您大大增加已删除项目的数量(我尝试了 10000000),那么它会永远继续插入/删除循环。我也对这里发生的事情感兴趣。
  • 好问题和好答案。对所有内容都投了赞成票。请您也添加一个指向 16.7M 限制的引用。
  • @x00 我在答案中添加了引用;这个解释了地图限制:stackoverflow.com/a/54466812/5260917

标签: javascript arrays maps v8


【解决方案1】:

我调整了您的脚本(见下文),以查看必须删除多少项目才能再次在 Map 中插入密钥。

结果是 8388608 (= 16777216/2) 和 node v12.18.1(基于 Chrome 的 V8 JavaScript 引擎构建)。

这让我想起了一种常见的模式,即当底层数据结构快满时,它的大小会翻倍。 所以我在 V8 引擎中寻找实际的 Map 实现。

这是 V8 开发 blog 所说的:

ECMAScript 2015 引入了几种新的数据结构,例如 Map、Set、WeakSet 和 WeakMap,它们都在底层使用了哈希表

V8 source code 中有一条有趣的评论:

HashTable 是 FixedArray 的子类,它实现了一个哈希表 使用开放寻址和二次探测。 为了使二次探测起作用,没有 尚未使用和已删除的元素是 杰出的。删除元素后继续探测 遇到未使用的元素时停止。 - key == undefined 的元素尚未使用。 - 带有 key == the_hole 的元素已被删除。

基本上,当脚本删除一个键时,它似乎只是被标记为已删除。正如 V8 代码注释所说,它变成了一个“洞”。 只有在引擎真正重建底层数据结构时才会真正删除它(这就是脚本删除一半元素时发生的情况)。

无论如何,这是我的理解。我们需要深入研究 V8 代码才能弄清所有细节。

其他有趣的参考资料:

map = new Map();
let i = 0;

while (true) {
  i++;

  try {
    map.set(i, i);
  } catch (e) {
    console.log(e);
    break;
  }

  if (i % 100000 === 0)
    console.log('inserted: ', i);
}

console.log('max map size:', map.size, 'inserted:', i);

let j = 0;
while (true) {
  j++;

  map.delete(j);

  if (j % 100000 === 0) {
    console.log('deleted: ', j, 'map size: ', map.size);

    if (map.size == 0) {
        break;
    }
  }

  try {
    map.set(i, i);
  } catch(e) {
    continue;
  }

  break;
}

console.log('deleted before inserting again: ', j);

【讨论】:

  • 这实际上是一个很好的答案,您已经完成了一些惊人的研究。我想我们需要一个 V8 开发人员来进一步澄清这一点。但是,当清理一半数据时,它们似乎确实清除了 Map 内存
  • 您可以在问题中添加“v8”标签以引起 V8 开发人员的注意。
  • 好主意!已添加!
【解决方案2】:

我研究了 ECMA 语言规范以查看 Maps (Link)。您看到的行为似乎与规范一致,并且来自 Map 的删除原型的规范定义。

当使用Map.prototype.delete(key)删除一个Map元素时,规范只要求匹配key的元素设置为空

这是从ECMA spec复制和粘贴的定义:

3.1.3.3 Map.prototype.delete ( key )

采取以下步骤:
  1. 设 M 为 this 值。
  2. 执行? RequireInternalSlot(M, [[MapData]]).
  3. 让条目是 M.[[MapData]] 的列表。
  4. 对于作为 entries 元素的每个 Record { [[Key]], [[Value]] } p,请执行
    一种。如果 p.[[Key]] 不为空且 SameValueZero(p.[[Key]], key) 为真,则 一世。将 p.[[Key]] 设置为空。
    ii.将 p.[[Value]] 设置为空。
    iii.返回真。
  5. 返回 false。

对我们来说最重要的部分是 4a。

删除元素时,Map.prototype.delete 检查每条记录 p 中是否存在 p.[[Key]] 与提供的 key匹配的元素> 论据。

找到后,p.[[Key]] 和 p.[[Value]] 都设置为空。 p>

这意味着,虽然 key 和 value 已经消失并且不再存储或检索,但存储 key 和 value 的空间,元素本身可能确实留在 Map 的存储中,并且仍然占用空间在幕后。

虽然规范包含以下关于其使用“空”的注释...

empty 用作规范设备以指示条目已被删除。实际实现可能会采取其他措施,例如从内部数据结构中物理删除条目。

...它仍然为实现简单地擦除数据而不回收空间而敞开大门,这显然是您的示例中发生的情况。

Map.prototype.set(key, value) 呢?

set() 的情况下,该函数首先检查具有匹配键以改变其值的现有元素,并跳过该过程中的所有空元素。如果没有找到,则“追加 p [] 作为条目的最后一个元素”。

那么,Map.prototype.size 呢?

size 的情况下,规范循环遍历地图中的所有元素,并简单地为它遇到的所有非空元素增加一个计数器。


我发现这真的很有趣......如果我不得不冒险猜测,我认为在大多数情况下,查找和删除空元素的开销被认为是不必要的,因为填充结构必须达到的数量是这么大,即。因为地图有这么多。我想知道删除一个空元素的时间和空间开销对于一个足够大的数据集需要有多大。

【讨论】:

  • 规范的摘录特别具有启发性,谢谢。我认为这解释了为什么删除键并不能真正为地图提供更多“空间”。我仍然很好奇为什么地图在删除 10M+ 键时会回收空间,但似乎规范中没有描述这种行为,并且可能是 Map 的特定底层实现的结果。
  • 不,规范不鼓励实现保留内存插槽。该列表数据结构纯粹是一种规范设备,用作描述可变结构的迭代顺序要求的最简单选择。事实上,“遍历所有元素”和“检查每个记录p”是forbidden by the time complexity requirements
  • (是的,规范没有详细说明内存释放/分配或垃圾收集行为,而是故意将其保持打开状态,从而允许内存泄漏的实现,但将其归因于条目列表会产生误导。)
  • 感谢您的链接 @Bergi,时间复杂度不是我研究的问题,它肯定会在这里发挥作用,说明为什么不立即完成车库收集。
  • 要重写我以前的 cmets,因为担心他们会觉得他们过于防御。我并不打算暗示规范鼓励无垃圾收集的实现,如果它是这样遇到的,而只是规范的这一方面可能有助于部分解释 Asker 观察到的行为,如果没有这个,这似乎很奇怪知识。
【解决方案3】:

请检查一下,我对代码进行了一些更改,现在它可以工作了,如果它仍然不能工作,请告诉我

我接受这不是最好的方法,但重新初始化地图 对象会让我们添加更多数据,但它也会变慢 运行速度, 请打开控制台查看输出

var map = new Map();
let i = 0;
var ke=[]
while (true) {
  i++;

  set(i, i,map.size);

  if (i % 1000 === 0)
    console.log('INSERTED: ', i, 'KEYS', 'MAP SIZE :', map.size);
}

function set(key, value,s) {

 
  if (s >= 16730000) {
     
   var arr= ke.slice(0, 10000)
   ke.splice(0, 10000)
   arr.forEach(key => map.delete(key));
    console.log('DELETED, current map size:', map.size);
    map= new Map(map);
  
  arr=[]
  }else{
    try {
        ke.push(key)
        map.set(key, value);
      } catch (e) {
        console.log('MAP SIZE:', map.size, 'INSERTED:', key);
        throw e;
      }
  }

 
}

【讨论】:

  • 我不明白这是如何回答这个问题的。 OP 不想清除整个地图,他们只想删除前几个值。它也没有解释为什么他们会出错。
  • 我不是在寻找问题的解决方案,而是寻找问题的答案。而且我想重用旧地图而不是创建新地图
猜你喜欢
  • 1970-01-01
  • 2014-02-22
  • 2017-11-23
  • 1970-01-01
  • 2016-06-18
  • 1970-01-01
  • 2020-12-18
  • 2014-01-20
  • 2011-06-22
相关资源
最近更新 更多