JavaScript性能长时间运行的任务

时间:2011-07-28 19:13:40

标签: javascript performance recursion

前几天我注意到了一个问题(Reducing Javascript CPU Usage),我很感兴趣。

基本上这个人想要逐个字符地加密一些文件。显然,一次性完成这一切将锁定浏览器。

他的第一个想法是一次大约1kb的字符串,然后暂停X ms,这样它就可以让用户在处理之间保持与页面的交互。他还考虑过使用webWorkers(最好的主意),但显然不是跨浏览器。

现在我真的不想进入为什么这在javascript中可能不是一个好主意。但我想知道我是否能提出解决方案。

我记得看过Douglas Crockford的视频at js conf。该视频与node.js和事件循环有关。但我记得他在谈论将长时间运行的函数分解为单个块,因此新调用的函数将进入事件循环的末尾。而不是通过长时间运行的任务阻塞事件循环,防止其他任何事情发生。

我知道这是一个值得我调查的解决方案。作为一名前端开发人员,我从未真正体验过JS中长时间运行的任务,并且热衷于了解如何分解它们以及它们的运行方式。

我决定尝试一个递归函数out,它从一个0ms的setTimeout内部调用。我认为这会在事件循环中为其他任何想要在运行时发生的事件提供中断。但我也认为,虽然没有其他任何事情你将得到最大的计算。

这是我想出的。

(我要为代码道歉。我正在试验台,所以这很快就脏了。)

function test(i, ar, callback, start){
    if ( ar === undefined ){
        var ar = [],
        start = new Date;
    };
    if ( ar.length < i ){
        ar.push( i - ( i - ar.length )  );
        setTimeout(function(){
            test( i, ar, callback, start);
        },0);
    }
    else {
        callback(ar, start);
    };
}

(您可以将此代码粘贴到控制台中,它将起作用)

本质上,函数所做的是获取一个数字,创建一个数组并调用自身,而array.length < number将数量推到目前为止。它将第一次调用中创建的数组传递给所有后续调用。

我测试了它,它似乎完全按预期工作。只有它的表现相当差。我用..测试了它。

(这不是性感的代码)

test(5000, undefined, function(ar, start ){ 
    var finish = new Date; 
    console.log(
        ar.length,
        'timeTaken: ', finish - start 
    ); 
});

现在我显然想知道完成需要多长时间,上面的代码需要大约20秒。现在在我看来,JS不应该花20秒来计算5000.加上它正在进行一些计算和处理以将项目推送到数组中这一事实。但仍然20多岁有点陡峭。

所以我决定同时产生几个,看看它是如何影响浏览器性能和计算速度的。

(代码没有任何性感)

function foo(){ 
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 1'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 2'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 3'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 4'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 5'  ) });
};

总共五个,同时运行并且不会导致浏览器挂起。

在过程结束后,所有结果几乎完全同时返回。所有人都需要大约21.5秒来完成。这比它自己慢了1.5秒。但是我正在窗口上移动我的鼠标,这些元素具有:hover效果,只是为了确保浏览器仍在响应,因此这可能会占1.5的开销。

因为这些功能显然是并行运行的,所以浏览器中还剩下更多的计算效果。

是否有人能够明智地解释这里发生的事情,并提供有关如何改善此类功能的详细信息?

只是发疯了我做到了..

function foo(){
    var count = 100000000000000000000000000000000000000;  
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 1'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 2'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 3'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 4'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 5'  ) });
};

我一直在写这篇文章一直在运行,并且仍然在努力。浏览器没有抱怨或挂起。我会在结束时添加完成时间。

4 个答案:

答案 0 :(得分:14)

setTimeout没有0ms的最小延迟。最小延迟在5ms-20ms范围内,取决于浏览器。

我自己的个人测试显示,setTimeout不会立即回到事件堆栈

Live Example

它再次被调用之前有一个最小的时间延迟

var s = new Date(),
    count = 10000,
    cb = after(count, function() {
        console.log(new Date() - s);    
    });

doo(count, function() {
    test(10, undefined, cb);
});
  • 并行运行10000个并行计数到10个需要500毫秒。
  • 运行100计数到10需要60ms。
  • 运行1计数到10需要40ms。
  • 运行1计数到100需要400毫秒。

似乎每个人setTimeout必须等待至少4毫秒再次被召唤。但那是瓶颈。个人延迟setTimeout

如果您并行安排100个或更多这些,那么它将起作用。

我们如何优化这个?

var s = new Date(),
    count = 100,
    cb = after(count, function() {
        console.log(new Date() - s);    
    }),
    array = [];

doo(count, function() {
    test(10, array, cb);
});

设置100在同一阵列上并行运行。这将避免主要瓶颈,即setTimeout延迟。

以上完成时间为2ms。

var s = new Date(),
    count = 1000,
    cb = after(count, function() {
        console.log(new Date() - s);    
    }),
    array = [];

doo(count, function() {
    test(1000, array, cb);
});

在7毫秒内完成

var s = new Date(),
    count = 1000,
    cb = after(1, function() {
        console.log(new Date() - s);    
    }),
    array = [];

doo(count, function() {
    test(1000000, array, cb);
});

并行运行1000个作业大致是最佳选择。但是你会开始遇到瓶颈。数到100万仍然需要4500毫秒。

答案 1 :(得分:8)

您的问题是与工作单元相比的开销问题。你的setTimeout开销非常高,而你的工作单元ar.push非常低。该解决方案是一种称为块处理的旧优化技术。不是每次调用处理一个UoW,而是需要处理一块UoW。 “块”的大小取决于每个UoW花费的时间以及每个setTimeout / call / iteration(在UI变得无响应之前)可以花费的最长时间。

function test(i, ar, callback, start){
if ( ar === undefined ){
    var ar = [],
    start = new Date;
};
if ( ar.length < i ){
    // **** process a block **** //
    for(var x=0; x<50 && ar.length<i; x++){
        ar.push( i - ( i - ar.length )  );
    }
    setTimeout(function(){
        test( i, ar, callback, start);
    },0);
}
else {
    callback(ar, start);
};
}

您必须处理最大的块,而不会导致用户出现UI /性能问题。前面的运行速度提高了约50倍(块的大小)。

这与我们使用缓冲区读取文件而不是一次读取一个字节的原因相同。

答案 2 :(得分:3)

只是一个假设......可能是因为你正在构建一个包含5000个递归实例的递归堆栈,代码是如此之慢?你的调用不是真正的递归,因为它是通过settimeout函数发生的,但你传递给它的函数是一个闭包,所以它必须存储所有的闭包上下文......

性能问题可能与管理内存的成本有关,这也可以解释,而你的上一次测试似乎会让事情变得更糟......

我没有尝试过解释器,但是看看计算时间是否与递归数量呈线性关系,或者不是......说:100,500,1000,5000递归可能会很有趣......

我尝试解决的第一件事就是不使用闭包:

setTimeout(test, 0, i, ar, callback, start);

答案 3 :(得分:0)

实际上是在谈论这个,你正在使用的是递归函数,而JavaScript现在没有“Tail End Recursive Calls”,这意味着解释器/引擎必须为每个调用保留堆栈帧,这很重。

为了优化解决方案,我会尝试将其变成一个即时执行函数,该函数在全局范围内调用。