【问题标题】:JavaScript Linked List Causing Garbage CollectionJavaScript 链表导致垃圾回收
【发布时间】:2016-12-27 10:34:46
【问题描述】:

我在 JavaScript 中实现了以下链表数据结构:

class Node {
  constructor(data, list) {
    this.data = data;
    this.list = list;
    this.prev = null;
    this.next = null;
  }

  remove() {
    if (this.prev) {
      this.prev.next = this.next;
    } else {
      this.list.start = this.next;
    }

    if (this.next) {
      this.next.prev = this.prev;
    } else {
      this.list.end = this.prev;
    }

    this.next = null;
    this.prev = null;
    this.list.length -= 1;
  }
}

class LinkedList {
  constructor() {
    this.end = null;
    this.start = null;
    this.length = 0;
  }

  append(data) {
    const node = new Node(data, this);

    if (!this.start) {
      this.start = node;
    }

    if (this.end) {
      node.prev = this.end;
      this.end.next = node;
    }

    this.end = node;
    this.length += 1;

    return data;
  }

  remove() {
    if (this.end) {
      return this.end.remove();
    }
  }

  *[Symbol.iterator]() {
    let current = this.start;
    while (current) {
      yield current;
      current = current.next;
    }
  }
}

module.exports = LinkedList;

我用它来更新动画列表:

static update(timeDelta) {
  for (let node of this.animations) {
    const animation = node.data;

    if (animation.animating) {
      animation.update(timeDelta);
    } else {
      node.remove();
    }
  }
}

node.remove() 行在我的游戏中造成了非常明显的延迟。我怀疑它正在触发垃圾收集。反常的,如果我注释掉node.remove()这一行,让链表永远增长,游戏就会顺利运行。

动画不断被添加和删除。我在动画update函数中添加了一些日志记录:

start iterating linked list
removing
ms elapsed:  0.45499999999992724
end iterating
start iterating linked list
removing
ms elapsed:  0.455000000000382
end iterating
start iterating linked list
removing
ms elapsed:  0.13000000000010914
end iterating
start iterating linked list
(13) updating
ms elapsed:  2.200000000000273
end iterating

您可以看到链表每秒迭代多次,偶尔会删除一个节点。

我怎样才能从我的列表中实现 O(1) 删除而不实际导致性能下降?

【问题讨论】:

  • 使用链表的目的是什么?不能只使用数组或对象吗?
  • 我可以使用数组,但我试图避免使用splice,这将不得不将所有元素转移过来。我想要恒定时间删除元素。
  • 然后就可以使用对象了。
  • 而不是取消引用数据从而导致 GC 命中。将对象放在另一个数组中,并将其用于您创建的下一个对象,假设它们具有相同的数据模式和大小。如果没有,那么您只需要保留数据,直到 GC 命中不会影响应用程序。
  • @Gothdo 你的意思是我应该将这些动画作为键值对存储在一个对象中,然后在它们上使用delete 关键字吗? @Blindman67 这是个好主意。所以这个链表抽象因为 GC 就没有意义了?

标签: javascript data-structures linked-list garbage-collection ecmascript-6


【解决方案1】:

node.remove() 行在我的游戏中造成了非常明显的延迟。我怀疑它正在触发垃圾收集。

不。延迟来自这样一个事实,即每个update 调用只更新很少的动画。

您的问题是古老而著名的“迭代期间删除”问题。在您的情况下,它不会触发罕见的边缘情况错误,它只是停止迭代:

while (current) {
  yield current;
  // after yielding, in the `update` function, we have
  //    node = current
  // and most importantly
  //    node.remove()
  // which assigns (with `this` being `node`)
  //    this.next = null;
  // then the iteration is resumed
  current = current.next;
}

哎呀。简单的解决方法是在 yield 之前缓存下一个要迭代的节点:

let next = this.start;
while (next) {
  const current = next;
  next = current.next;
  yield current;
}

(或类似的东西),但当然当下一个节点被删除时它仍然会失败。更好的方法可能是省略这些行

this.next = null;
this.prev = null;

来自节点的remove 方法,以便在删除期间引用保持不变。这不会影响 GC。


另一种解决方案是完全删除链接列表 - 它被过度设计,除非您经常在列表中间添加/删除节点在迭代之外。在迭代期间过滤掉旧动画很简单,它可以用一个很好的旧(内存效率高?)Array,甚至就地完成:

function filter(array, callback) {
    var i=0, j=0;
    while (j < array.length) {
        if (callback(array[j]))
            array[i++] = array[j++];
        else
            array[i] = array[j++];
    }
    array.length = i;
}
function update(timeDelta) {
    filter(animations, animation => {
        var keep = animation.animating;
        if (keep) animation.update(timeDelta);
        return keep;
    });
}

(您可以通过在 i===j 时不重新分配来优化 filter

【讨论】:

  • 啊,不错。我目前正在使用您缓存下一个节点的简单修复。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-26
  • 2011-01-22
  • 2011-02-12
  • 2011-09-11
  • 2011-12-21
相关资源
最近更新 更多