在V8中慢慢删除JS中的对象属性

时间:2017-04-24 17:11:56

标签: javascript performance v8

为了训练自己一些打字稿,我写了simple ES6 Map+Set-like implementation based on plain JS Object。它只适用于原始键,所以没有桶,没有哈希码等。我遇到的问题是实现删除方法。使用普通delete只是速度慢得令人无法接受。对于大型地图,它比ES6 Map删除慢约300-400x。我注意到如果对象的大小很大,性能会大幅下降。在节点JS 7.9.0(例如Chrome 57)上,如果对象具有50855属性delete,则性能与ES6 Map相同。但是对于50856属性,ES6 Map在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或一些保护对象,但这会使代码混乱,因为hasOwnProperty将无法正常工作,{{1循环将需要额外的检查等等。还有更好的解决方案吗?

P.S。我在OSX Sierra上使用节点7.9.0

被修改 感谢评论家伙,我修复了OP / S =&gt; KOP / S。我想我问了一个相当严重的问题,所以我改了标题。经过一些调查后我发现,例如在Firefox中没有这样的问题 - 删除成本线性增长。所以这是超级智能V8的问题。我认为这只是一个错误:(

2 个答案:

答案 0 :(得分:16)

(V8开发人员在这里。)是的,这是一个已知问题。根本问题是,当对象变得过于稀疏时,对象应该将它们的元素支持存储从平面数组切换到字典,并且历史上实现的方式是每个delete操作检查是否仍然存在足够的元素为了那个过渡发生了。阵列越大,检查所用的时间就越多。在某些条件下(最近创建的对象低于特定大小),跳过了检查 - 结果令人印象深刻的加速是您在fast()案例中观察到的。

我借此机会修复了常规/慢速路径的(坦率地说很愚蠢)行为。应该每隔一段时间检查一次,而不是每一次delete检查。修复程序将在V8 6.0中,应该在几个月内被Node接收(我相信Node 8应该在某个时候得到它)。

也就是说,在许多情况下使用delete导致各种形式和幅度的减速,因为它往往会使事情变得更复杂,迫使引擎(任何引擎)执行更多检查和/或脱落各种快速路径。通常建议尽可能避免使用delete。由于您有ES6地图/集,请使用它们! : - )

答案 1 :(得分:2)

回答“为什么在N中添加1会减慢删除操作”的问题。

我的猜测:缓慢来自为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 ,只需要在你的对象上增加一个键。

现在,我说我“猜”这是缓慢来自的地方,但我认为随着对象的大小增加,删除功能会变慢。

如果您尝试使用N = 70855,则仍会使用相同的 8.9MB 。这是因为通常内存分配器在固定的“批处理”中分配内存,同时增加数组/对象的大小,以减少它们执行的内存分配数量。

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

(如果你想了解更多内容,你应该阅读有关动态数组的内存分配;有一篇很酷的文章说明你应该使用什么增加率进行内存分配,我找不到atm :()

PS:delete不是“非常慢”,你只是计算op / s错误。传递的时间以毫秒为单位,而不是以秒为单位,因此您必须乘以1000