在 V8 中缓慢删除 JS 中的 object 属性

Slow delete of object properties in JS in V8

为了训练自己一点 Typescript,我写了一个 simple ES6 Map+Set-like implementation based on plain JS Object。它只适用于原始键,所以没有桶,没有 hash-codes,等等。我遇到的问题是实现 delete 方法。使用普通的 delete 速度慢得令人无法接受。对于大型地图,它比 ES6 地图删除慢 300-400 倍。我注意到如果 object 的大小很大,性能会大幅下降。在 Node JS 7.9.0 上(例如 Chrome 57)如果 object 有 50855 个属性 delete 性能与 ES6 Map 相同。但是对于 50856 个属性,ES6 映射要快 2 个数量级。这是要重现的简单代码:

// for node 6: 76300
// for node 7: 50855
const N0 = 50855;

function fast() {
 const N = N0

 const o = {}
 for ( let i = 0; i < N; i++ ) {
  o[i] = i
 }

 const t1 = Date.now()
 for ( let i = 0; i < N; i++ ) {
  delete o[i]
 }
 const t2 = Date.now()

 console.log( N / (t2 - t1) + ' KOP/S' )
}

function slow() {
 const N = N0 + 1 // adding just 1

 const o = {}
 for ( let i = 0; i < N; i++ ) {
  o[i] = i
 }

 const t1 = Date.now()
 for ( let i = 0; i < N; i++ ) {
  delete o[i]
 }
 const t2 = Date.now()

 console.log( N / (t2 - t1) + ' KOP/S' )
}

fast()

slow()

我想我可以将 delete 属性设置为 undefined 或某些保护 object,但这会弄乱代码,因为 hasOwnProperty 不会正常工作,for...in 循环将需要额外检查等等。还有更好的方案吗?

P.S。我在 OSX Sierra

上使用节点 7.9.0

已编辑 感谢大家的评论,我修复了 OP/S => KOP/S。我想我问的问题不明确,所以我更改了标题。经过一些调查,我发现例如在 Firefox 中没有这样的问题——删除成本线性增长。所以这是超级智能V8的问题。我认为这只是一个错误:(

回答问题"why adding 1 to N slows the delete operation"。

我的猜测:缓慢来自于为您的 Object.

分配内存的方式

尝试将您的代码更改为:

(() => {
    const N = 50855

    const o = {}
    for ( let i = 0; i < N; i++ ) {
        o[i] = i
    }

    // Show the heap memory allocated
    console.log(process.memoryUsage().heapTotal);
    const t1 = Date.now()
    for ( let i = 0; i < N; i++ ) {
        delete o[i]
    }
    const t2 = Date.now()

    console.log( N / (t2 - t1) + ' OP/S' )
})();

现在,当您 运行 和 N = 50855 分配的内存是:“8306688 字节”(8.3MB)

当你 运行 和 N = 50856 分配的内存是:“8929280 字节”(8.9MB)。

所以你得到了 600kb 分配的内存大小增加,只需向你的对象添加一个键。

现在,我说我 "guess" 这就是速度变慢的原因,但我认为删除功能随着对象大小的增加而变慢是有道理的。

如果您尝试使用 N = 70855,您仍然会使用相同的 8.9MB。这是因为通常内存分配器在固定 "batches" 中分配内存,同时增加 Array/Object 的大小以减少它们所做的内存分配次数。

现在,deleteGC 可能会发生同样的事情。您 删除 的内存必须由 GC 拾取,如果对象大小较大, GC 会更慢。如果键的数量低于特定数量,也可能会释放内存。

(如果您想了解更多信息,您应该阅读动态数组的内存分配;有一篇很棒的文章介绍了内存分配应该使用的增长率,我在 atm 找不到它:( )

PS:delete不是"extremely slow",你只是计算错了op/s。经过的时间以毫秒为单位,而不是以秒为单位,因此您必须乘以 1000.

(此处为 V8 开发人员。)是的,这是一个已知问题。潜在的问题是当对象变得太稀疏时,对象应该将它们的元素后备存储从平面数组切换到字典,并且历史上实现的方式是每个 delete 操作检查是否仍然存在足够的元素那个转变还没有发生。数组越大,检查所花的时间就越多。在特定条件下(最近创建的对象小于特定大小),检查被跳过 - 由此产生的令人印象深刻的加速是您在 fast() 案例中观察到的。

我借此机会修复了 regular/slow 路径的(坦率地说相当愚蠢的)行为。时不时地检查一下就足够了,而不是每一个 delete。修复将在 V8 6.0 中进行,Node 应该会在几个月内采用它(我相信 Node 8 应该会在某个时候得到它)。

也就是说,使用 delete 在许多情况下会导致各种形式和幅度的减速,因为它会使事情变得更复杂,迫使引擎(any 引擎) 执行更多检查 and/or 脱离各种快速路径。通常建议尽可能避免使用 delete。既然你有 ES6 maps/sets,就用它们吧! :-)