【问题标题】:node.js splice too slow for more than 70000 items对于超过 70000 个项目,node.js 拼接速度太慢
【发布时间】:2018-03-09 03:30:57
【问题描述】:

我是 node.js 的新手。

我尝试在数组中插入 70000 个项目,然后将它们全部删除:

var Stopwatch = require("node-stopwatch").Stopwatch;
var stopwatch = Stopwatch.create();


var a = []
stopwatch.start();

for (var i = 1 ; i < 70000 ; i++){
    a.push((parseInt(Math.random() * 10000)) + "test");
}

for (var i = 1 ; i < 70000 ; i++){
    a.splice(0,1);
}

stopwatch.stop();

console.log("End: " + stopwatch.elapsedMilliseconds + " : " + a.length);

它工作正常,输出是:

PS C:\Users\Documents\VSCode> node test.js
End: 51 : 0

但是当我将项目数量增加到 72000 时,结束需要太多时间:

var Stopwatch = require("node-stopwatch").Stopwatch;
var stopwatch = Stopwatch.create();


var a = []
stopwatch.start();

for (var i = 1 ; i < 72000 ; i++){
    a.push((parseInt(Math.random() * 10000)) + "test");
}

for (var i = 1 ; i < 72000 ; i++){
    a.splice(0,1);
}

stopwatch.stop();

console.log("End: " + stopwatch.elapsedMilliseconds + " : " + a.length);

输出是:

End: 9554 : 0

为什么会发生?只增加了2000件,但是太费时间了。

Node.js 版本为:v6.11.3

【问题讨论】:

  • 你知道时间爆炸是发生在数组的填充还是破坏,还是两者兼而有之?
  • @apsillers 在破坏中'a.splice(0,1); '
  • 刚刚在调查/玩弄这个 - 对我来说上限是 71109 - 在这之后,它真的很慢。以 71110 为例,耗时 12476 毫秒! 71109 是 64 毫秒
  • 查看v8 source code for arrays,我看到v8内部有两个不同的splice操作。一种是完整的普通数组;另一个用于稀疏数组 (['foo',,,,,'bar',,,,]) 和对象。出于某种原因,v8 可能会使用其较慢的回退 splice 来处理更大的数组。
  • Win7x32,节点 5.11.1:在 172000 上减速。

标签: arrays node.js v8


【解决方案1】:

V8 开发人员在这里。在开头(array[0])删除(或插入)数组元素通常非常昂贵,因为必须移动所有剩余的元素。本质上,对于这些.splice(0, 1) 操作中的每一个,引擎必须在后台执行以下操作:

for (var j = 0; j < a.length - 1; j++) {
  a[j] = a[j+1];
}
a.length = a.length - 1`

在某些情况下,V8 可以在底层使用一个技巧,即移动对象的开头——在快速的情况下,您可以看到这个技巧提供的惊人的加速。然而,由于技术原因,这个技巧不能应用于超过一定大小的数组。由此产生的“减速”实际上是这种非常昂贵的操作的“真实”速度。

如果您想快速删除数组元素,请从末尾删除它们(array[array.length - 1]),例如使用Array.pop()。如果要一次性删除所有元素,只需设置array.length = 0。如果您需要快速 FIFO/“队列”语义,请考虑从环形缓冲区中获取灵感:为要读取/返回的下一个元素设置一个“光标”,并且仅在需要释放大量元素时才缩小数组。大致:

function Queue() {
  this.enqueue = function(x) {
    this.array_.push(x);
  }
  this.dequeue = function() {
    var x = this.array_[this.cursor_++];
    // Free up space if half the array is unused.
    if (this.cursor_ > this.array_.length / 2) {
      this.array_.splice(0, this.cursor_);
      this.cursor_ = 0;
    }
    return x;
  }
  this.array_ = [];
  this.cursor_ = 0;
}

旁注:这里没关系,但为了记录,要将 70,000 个元素推入数组,循环应该从 0 开始:for (var i = 0; i &lt; 70000; i++) {...}。如前所述,您只推送了 69,999 个元素。

旁注2:通过“parseInt”将双精度数舍入为整数非常慢,因为它首先将双精度数格式化为字符串,然后将该字符串作为整数读回。更快的方法是Math.floor(Math.random() * 10000))。 (为了这个测试的目的,你也可以简单地推送i。)

【讨论】:

    【解决方案2】:

    有趣,我做了类似的事情

    if (i % 1000 === 0) {
        console.log(i + " " + stopwatch.elapsedMilliseconds + " : " + a.length);
    }
    

    在第二个循环内。 (计算起来很痛苦,但它有助于诊断问题)

    我没有遭受性能损失。但是,我认为,我发现为什么“只”多做 2000 件事情对节点来说很难。首先 - 我的区别: [loop max num, unit, 3 benchmarks 结果]

    70k: [ms] ~26k, ~25.7k, ~26k 72k: [ms] ~25.6k, 27k, 25.7k

    好的,当我看到日志时,我看到了,最后 10k 记录是即时计算的。我认为splice 从前面删除了 1 个项目,然后 - 一项一项地将数组 1 索引“移到开头”,让我们将测试更改为 10 个数组,每个数组有 10k 条记录,看看它是否会更好。我会用最懒的方式来做:

    var Stopwatch = require("node-stopwatch").Stopwatch;
    var stopwatch = Stopwatch.create();
    
    var a1 = [], a2 = [], a3 = [], a4 = [], a5 = [];
    var a6 = [], a7 = [], a8 = [], a9 = [], a10 = [];
    
    stopwatch.start();
    
    function fill (arr) {
        for (var i = 1 ; i < 10000 ; i++){
            arr.push((parseInt(Math.random() * 10000)) + "test");
        }
    }
    
    fill(a1); fill(a2); fill(a3); fill(a4); fill(a5);
    fill(a6); fill(a7); fill(a8); fill(a9); fill(a10);
    
    let removeCount = 0;
    function unfill(arr) {
        for (var i = 1 ; i < 10000 ; i++){
            arr.splice(0,1);
            removeCount++;
    
            if (i % 1000 === 0) {
                console.log(i + " " + stopwatch.elapsedMilliseconds + " : " + arr.length);
            }
        }
    }
    
    unfill(a1); unfill(a2); unfill(a3); unfill(a4); unfill(a5);
    unfill(a6); unfill(a7); unfill(a8); unfill(a9); unfill(a10);
    
    stopwatch.stop();
    
    console.log("End: " + stopwatch.elapsedMilliseconds + " removeCount " + removeCount);
    

    而且,是的......我没有回答为什么你的电脑在 70k 和 72k 记录之间会有这样的性能损失 - 我相信这取决于机器......可能缺少 RAM,但不要弄错 -我不知道。

    我解决了如何改进它。 10 个阵列上的 100k(-10) 执行时间约为 73-74 毫秒。我认为,您可以将其写入二维数组并修改逻辑以根据需要计算长度和其余部分。

    感谢关注。

    【讨论】:

    • “可能缺少内存”不,不是内存不足,我有 6GB 内存,其中大约 70% 是免费的。
    猜你喜欢
    • 2018-07-03
    • 1970-01-01
    • 2018-03-14
    • 2017-02-28
    • 2023-01-19
    • 1970-01-01
    • 2015-06-06
    • 2011-10-19
    • 1970-01-01
    相关资源
    最近更新 更多