为什么nodejs数组移位/推送操作的速度在数组大小上不是线性的?在87370处有一个戏剧性的膝盖完全压碎了系统。
首先尝试使用q中的87369个元素,然后使用87370.(或者,在64位系统上,尝试85983和85984.)对我来说,前者运行时间为.05秒;后者,在80秒内 - 慢1600倍。 (在节点v0.10.29的32位debian linux上观察到)
q = [];
// preload the queue with some data
for (i=0; i<87369; i++) q.push({});
// fetch oldest waiting item and push new item
for (i=0; i<100000; i++) {
q.shift();
q.push({});
if (i%10000 === 0) process.stdout.write(".");
}
64位debian linux v0.10.29从85984开始爬行,并在.06 / 56秒内运行。节点v0.11.13具有类似的断点,但是具有不同的数组大小。
答案 0 :(得分:3)
Shift是一个非常慢的数组操作,因为你需要移动所有元素,但是当数组内容适合页面(1mb)时,V8能够使用技巧快速执行它。
空数组从4个插槽开始,当你继续推动时,它将使用公式1.5 * (old length + 1) + 16
调整数组的大小。
var j = 4;
while (j < 87369) {
j = (j + 1) + Math.floor(j / 2) + 16
console.log(j);
}
打印:
23
51
93
156
251
393
606
926
1406
2126
3206
4826
7256
10901
16368
24569
36870
55322
83000
124517
所以你的数组大小最终实际上是124517项,这使得它太大了。
您实际上可以将阵列预分配到正确的大小,它应该能够再次快速移动:
var q = new Array(87369); // Fits in a page so fast shift is possible
// preload the queue with some data
for (i=0; i<87369; i++) q[i] = {};
如果您需要大于此值,请使用the right data structure
答案 1 :(得分:1)
我开始深入研究v8源代码,但我仍然不理解它。
我检测了deps / v8 / src / builtins.cc:MoveElemens(从Builtin_ArrayShift调用,用memmove实现移位),它清楚地显示了减速:每秒只有1000个移位,因为每个移位需要1ms:
AR: at 1417982255.050970: MoveElements sec = 0.000809
AR: at 1417982255.052314: MoveElements sec = 0.001341
AR: at 1417982255.053542: MoveElements sec = 0.001224
AR: at 1417982255.054360: MoveElements sec = 0.000815
AR: at 1417982255.055684: MoveElements sec = 0.001321
AR: at 1417982255.056501: MoveElements sec = 0.000814
其中memmove为0.000040秒,批量为heap-&gt; RecordWrites(deps / v8 / src / heap-inl.h):
void Heap::RecordWrites(Address address, int start, int len) {
if (!InNewSpace(address)) {
for (int i = 0; i < len; i++) {
store_buffer_.Mark(address + start + i * kPointerSize);
}
}
}
是(store-buffer-inl.h)
void StoreBuffer::Mark(Address addr) {
ASSERT(!heap_->cell_space()->Contains(addr));
ASSERT(!heap_->code_space()->Contains(addr));
Address* top = reinterpret_cast<Address*>(heap_->store_buffer_top());
*top++ = addr;
heap_->public_set_store_buffer_top(top);
if ((reinterpret_cast<uintptr_t>(top) & kStoreBufferOverflowBit) != 0) {
ASSERT(top == limit_);
Compact();
} else {
ASSERT(top < limit_);
}
}
当代码运行缓慢时,会有一些shift / push操作,然后为每个MoveElements运行5-6次调用Compact()
。当它快速运行时,MoveElements直到最后几次才被调用,并且在完成时只进行一次压缩。
我猜测内存压缩可能会颠簸,但它还没有到位。
编辑:忘记关于输出缓冲工件的最后一次编辑,我正在过滤重复项。
答案 2 :(得分:1)
这个错误已经报告给谷歌,谷歌在没有研究这个问题的情况下关闭了它。
https://code.google.com/p/v8/issues/detail?id=3059
从队列(数组)中移出并调用任务(函数)时 GC(?)停顿了很长时间。
114467班次没问题 114468班次有问题,出现症状
回应:
GC与此无关,也没有任何事情停滞不前。Array.shift()是一项昂贵的操作,因为它需要所有数组 要移动的元素。对于堆的大多数区域,V8已经实现了 隐藏此成本的特殊技巧:它只是将指针碰到了 一个对象的开头,有效地切断了第一个 元件。但是,当一个数组太大而必须放入它时 &#34;大对象空间&#34;,这个技巧不能作为对象启动应用 必须对齐,所以在每个.shift()操作中,所有元素都必须 实际上是在记忆中移动。
我不确定我们能做些什么。如果你想要一个 &#34;队列&#34; JavaScript中的对象,具有保证的O(1)复杂度 .enqueue()和.dequeue()操作,你可能想要实现你的 自己的。
编辑:我抓住了微妙的#34;所有元素都必须被移动&#34; part - 是RecordWrites不是GC而是实际的元素副本吗?数组内容的memmove是0.04毫秒。 RecordWrites循环是1.1 ms运行时的96%。
编辑:如果&#34;对齐&#34;意味着第一个对象必须是第一个地址,这是memmove的作用。什么是RecordWrites?