为了训练自己一些打字稿,我写了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的问题。我认为这只是一个错误:(
答案 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 。这是因为通常内存分配器在固定的“批处理”中分配内存,同时增加数组/对象的大小,以减少它们执行的内存分配数量。
现在,delete
和GC
也可能发生同样的事情。您删除的内存必须由GC选取,如果对象大小较大,GC
将会变慢。如果密钥数量低于特定数量,则可能会释放内存。
(如果你想了解更多内容,你应该阅读有关动态数组的内存分配;有一篇很酷的文章说明你应该使用什么增加率进行内存分配,我找不到atm :()
PS:delete
不是“非常慢”,你只是计算op / s错误。传递的时间以毫秒为单位,而不是以秒为单位,因此您必须乘以1000
。