微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何在不分配的情况下迭代 Javascript Map 或 Object?

如何解决如何在不分配的情况下迭代 Javascript Map 或 Object?

我正在用 Javascript 开发游戏,这涉及尽快运行渲染循环。我注意到 GC(垃圾收集器)峰值中断了平滑的帧速率,在分析之后我发现我的实体查询系统正在生成大量垃圾

这样做时,我发现了 Javascript 中的 iterators cause allocations。在我的代码的一部分中,我正在做这样的事情:

let minimumSet = this.getSmallestEntitySet(componentTypes);

for (let entity of minimumSet) {
  // handle entity
}

不幸的是,仅仅执行 for of 循环会导致分配(因为它每次运行循环时都会创建一个新的迭代器对象),并且由于它经常运行,因此会产生大量垃圾。我环顾四周,但不知道是否有办法在不执行分配的情况下迭代 SetMapObject。例如,使用 Javascript 数组,您可以迭代它,同时避免像这样的分配:

for (let i = 0; i < arr.length; i++) {
  let item = arr[i];

  // handle item
}

但是我找不到只用常规的 for 循环来像这样迭代 MapSet方法。可能吗?

假设这是不可能的,我认为解决这个问题的方法是使用排序数组而不是映射,对吗?唯一的问题是插入和删除O(log n) 而不是 O(1),但我认为这可能是值得的,只是为了避免分配。

有没有办法在不执行分配的情况下迭代这些集合,这是我最好的选择吗?

解决方法

(此处为 V8 开发人员。)

正如您所链接的问题所指出的,JavaScript 引擎(至少 V8)确实优化了迭代协议的规范文本所讨论的临时对象;尽管完整的答案(通常如此)是“视情况而定”。

一个效果是优化编译器会做的事情是优化,而那些不会立即启动(因为通常情况下,这会浪费精力)。所以如果你只运行一个函数几次,你会看到它的未优化版本分配了短暂的垃圾。但这很快就会改变(具体取决于引擎),一旦该功能被认为是“热门”并得到优化。

您可能遇到的另一个问题是直接迭代 Map(如:let map = new Map(); ...; for (let e of map) {...} 被指定为每次都将 e 作为数组 [key,value] 返回。这个数组没有被优化掉;它必须在每次迭代时分配。但是如果你只对处理值感兴趣,你可以避免通过只迭代值来创建它:for (let v of map.values()) {...} 不分配任何短期对象。迭代 map.keys() 也是如此。

如果您同时需要键和值,您可以将键上的迭代与地图查找结合起来:for (let key of map.keys()) { let value = map.get(key); ...},但是这比对值进行迭代要慢得多。如果您的对象在您的回答中实现了 interface Entry,即它们有一个携带其密钥的属性,那么您可以使用它:for (let value of map.values()) { let key = value.id; ...}


总而言之:如果 SparseSet 解决方案适合您,那当然也很酷。你甚至可以让它更高效一点:如果你将 add 改为 add(item: T) { let key = item.id; ...} 并更新 delete 以包含 this.sparse.delete(key),那么集合本身可以保证它的内部数据是始终一致,然后 contains 可以像 return this.sparse.get(key) !== undefined; 一样简单。

我认为解决这个问题的方法是使用排序数组而不是地图,对吗?唯一的问题是插入和删除是 O(log n)

在已排序数组上插入和删除是 O(n),因为您可能需要移动整个内容。 (如果您使用二进制搜索,则通过其排序键查找元素需要 O(log n)。)但是,使用 unsorted 数组可能比直觉上预期的要快,只要它们仍然很小(大约有几十个条目)。类似的东西:

class UnsortedArray<T extends Entry> {
  contents: Array<T> = [];

  add(item: T) { contents.push(T); }  // optional: check for duplicates first
  private find(key: number) {
    for (let i = 0; i < contents.length; i++) {
      if (contents[i].id == key) return i;
    }
    return -1;
  }
  size() { return contents.length; }
  get_by_index(index: number) { return contents[index]; }
  get_by_key(key: number) { return contents[find(key)]; }
  has(item: T) { return find(T.id) >= 0; }
  delete(item: T) {
    let index = find(T.id);
    if (index < 0) return;
    let last = contents.pop();
    if (index !== contents.length) contents[index] = last;
  }
}

这使您可以在 O(1) 中插入、无开销的经典迭代 (for (let i = 0; i < unsorted_array.size(); i++) { let entry = unsorted_array.get_by_index(i); ...})、删除和 has 中的 O(n)。一旦超过 30-50 个元素,我预计 has 实际上只会比对有序数组进行二分搜索慢;当然,has 性能是否重要取决于您的用例。

,

我想到的临时解决方法,直到找到更好的方法。基本上,只使用稀疏集。

interface Entry {
  id: number;
}

export class SparseSet<T extends Entry> {
  dense: Array<T>;
  sparse: Map<number,number>;
  
  constructor() {
    this.dense = [];
    this.sparse = new Map<number,number>();
  }

  get size() {
    return this.dense.length;
  }
  
  contains(key: number) {
    let denseIndex = this.sparse.get(key);
  
    if (denseIndex === undefined || denseIndex >= this.dense.length) {
      return false;
    }
  
    let denseItem = this.dense[denseIndex];
  
    return denseItem.id === key;
  }

  delete(key: number) {
    if (!this.contains(key)) { return; }

    let denseIndex = this.sparse.get(key);

    let lastItem = this.dense.pop();

    if (this.dense.length > 0) {
      this.dense[denseIndex] = lastItem; 
      
      this.sparse.set(lastItem.id,denseIndex);
    }
  }

  add(key: number,item: T) {
    if (this.contains(key)) { return; }
  
    this.sparse.set(key,this.dense.length);
    this.dense.push(item);
  }
}

然后你可以像这样添加和删除:

let entity = new Entity();
let items = new SparseSet<Entity>();
items.add(entity.id,entity);
items.delete(entity.id);

并在不执行分配的情况下进行迭代:

for (let i = 0; i < items.size; i++) {
  let item = items.dense[i];
} 

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。