为什么nodejs数组移位/推送循环比数组长度87369慢1000倍?

时间:2014-12-07 09:34:17

标签: node.js performance v8

为什么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具有类似的断点,但是具有不同的数组大小。

3 个答案:

答案 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?