Node.js 为什么nodejs阵列移位/推送循环比阵列长度87369慢1000倍?

Node.js 为什么nodejs阵列移位/推送循环比阵列长度87369慢1000倍?,node.js,performance,v8,Node.js,Performance,V8,为什么nodejs阵列移位/推送操作的速度在阵列大小中不是线性的?在87370处有一个戏剧性的膝盖,完全粉碎了这个系统 试试这个,先用q中的87369个元素,然后用87370。(或者,在64位系统上,试试85983和85984。)对我来说,前者运行时间为0.05秒;后者,在80秒内——慢1600倍。(在节点为v0.10.29的32位debian linux上观察到) q=[]; //用一些数据预加载队列 对于(i=0;i我开始深入研究v8源代码,但我仍然不理解它 我检测了deps/v8/src/

为什么nodejs阵列移位/推送操作的速度在阵列大小中不是线性的?在87370处有一个戏剧性的膝盖,完全粉碎了这个系统

试试这个,先用q中的87369个元素,然后用87370。(或者,在64位系统上,试试85983和85984。)对我来说,前者运行时间为0.05秒;后者,在80秒内——慢1600倍。(在节点为v0.10.29的32位debian linux上观察到)

q=[];
//用一些数据预加载队列

对于(i=0;i我开始深入研究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秒,bulk是heap->RecordWrites(deps/v8/src/heap inl.h):

void Heap::RecordWrites(地址、int start、int len){
如果(!InNewSpace(地址)){
对于(int i=0;i
即(存储缓冲区inl.h)

void StoreBuffer::Mark(地址addr){
断言(!heap_u->cell_space()->Contains(addr));
断言(!heap_uu->code_uspace()->Contains(addr));
地址*top=reinterpret\u cast(heap\uu->store\u buffer\u top());
*top++=addr;
堆->公共设置存储缓冲区顶部(顶部);
if((重新解释强制转换(顶部)&kStoreBufferOverflowBit)!=0){
断言(top==极限值);
紧凑型();
}否则{
断言(顶部<极限值);
}
}
当代码运行缓慢时,会运行shift/push操作,然后对每个MoveElements运行5-6次调用
Compact()
。当代码运行快速时,MoveElements直到最后几次才被调用,当它完成时只调用一次压缩

我猜内存压缩可能会带来冲击,但对我来说还没有到位


编辑:忘记上次关于输出缓冲工件的编辑,我是在过滤重复项。

Shift对于数组来说是一个非常慢的操作,因为您需要移动所有元素,但是V8能够使用一个技巧在数组内容适合页面(1mb)时快速执行它

空数组从4个插槽开始,当您继续按下时,它将使用公式
1.5*(旧长度+1)+16调整数组大小

var j = 4;
while (j < 87369) {
    j = (j + 1) + Math.floor(j / 2) + 16
    console.log(j);
}
所以你的数组大小实际上是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] = {};
var q=new Array(87369);//适合一个页面,因此可以快速移动
//用一些数据预加载队列

对于(i=0;i这个bug已经报告给了谷歌,谷歌没有研究这个问题就关闭了它

从队列(数组)移出并调用任务(函数)时 GC(?)暂停的时间过长

114467班还可以 114468班次有问题,出现症状

答复如下:

他与此无关,也没有任何拖延

shift()是一个昂贵的操作,因为它需要所有的数组 要移动的元素。对于堆的大多数区域,V8实现了 隐藏此成本的特殊技巧:它只是将指针指向 由一个物体开始,有效地切断了第一个物体 但是,当数组太大,必须将其放入 “大对象空间”,此技巧不能在对象启动时应用 必须对齐,因此在每个.shift()操作中,所有元素都必须对齐 实际上是在记忆中移动

我不确定我们能做多少,如果你想 JavaScript中的“Queue”对象,对于 .enqueue()和.dequeue()操作,您可能希望实现 拥有

编辑:我刚刚捕捉到了微妙的“所有元素都必须移动”部分——RecordWrites不是GC而是实际的元素副本吗?数组内容的memmove是0.04毫秒。RecordWrites循环是1.1毫秒运行时间的96%


编辑:如果“对齐”意味着第一个对象必须位于第一个地址,memmove就是这么做的。什么是记录写入?

我在85983(5,不是6)中修复了输入错误,谢谢,但问题在于
shift
,而不是
unshift
——数组越来越小(它已通过附件预先分配到87369)。v8正在使用memmove()移动它们只需0.04毫秒;总时间为1.10毫秒,慢25倍。减速来自内部记录写入。在这种情况下为什么需要记录写入?它有什么作用?我尝试了你的建议,将其更改为
q=new Array(20000)
,但速度仍然很慢。您无法使用
预分配。按
,它将动态调整大小,当数字变大时,调整大小非常方便,因为调整大小操作非常昂贵(需要将所有元素复制到新数组)但我不是计时推送,我是计时移位。我的观点是,到时间移位运行时,阵列将已分配。使用前的分配是预分配。当移位运行时,阵列已达到最大大小;它不会变得更大。由于动态调整大小,开始移位时的阵列大小为124517,因此阵列将是对于新空间来说太大,无法实现快速移动。我建议预先分配准确的大小,因为87000个项目适合新空间。在v8使用的分代垃圾收集中,记录写入在某种意义上是一个钩子,用于检查是否正在使旧空间中的对象指向新空间中的对象。啊,这很有用,谢谢。mem也是如此move()移动数组中的指针或对象本身?没有创建新对象,也没有对列表进行gc
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_);
  }
}
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 
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] = {};