为什么在这种情况下,原生承诺似乎比Chrome中的回调更快?

3
这里是 jsperf: http://jsperf.com/promise-vs-callback 回调函数案例(211 操作/秒):
// async test
var d = deferred;

function getData(callback) {
  setTimeout(function() {
    callback('data')
  }, 0)
}

getData(function(data) {
  d.resolve()
})

Promise案例(614次操作/秒):

// async test
var d = deferred;

function getData() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve('data')
    }, 0);
  })
}

getData().then(function(data) {
  d.resolve()
})

正如你所看到的,Promise比Deferred更快,但它们的代码更多。问题是为什么会这样。

在这里,deferred被JSPerf定义为异步测试的完成。


哥们,从什么时候开始,“更多的代码”就意味着“性能更差”了?有时为了确保某个东西根据底层机制的具体要求表现良好,需要编写大量代码以实现更好的性能。 - daniel.gindi
3个回答

6
似乎 Chrome 设置 setTimeout(fn, 0) 的最小延迟是关键所在。我搜索了一下,找到了这个:https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/Hn3GxRLXmR0/XP9xcY_gBPQJ 我引用其中的重要部分:

定时器夹紧的工作方式是每个任务都有一个关联的定时器嵌套级别。如果任务源自 setTimeout() 或 setInterval() 调用,则嵌套级别比调用 setTimeout() 或最近一次 setInterval() 迭代的任务高一级,否则为零。只有当嵌套级别达到4或更高时,才会应用4毫秒的夹紧。在事件处理程序、动画回调或不深度嵌套的定时器上设置的定时器不受夹紧的影响。

在回调函数中,setTimeout会递归调用,在另一个setTimeout的上下文中,因此最小超时时间为4ms。 在Promise情况下,实际上并没有递归调用setTimeout,因此最小超时时间为0(实际上不会是0,因为还有其他内容需要运行)。
那么我们如何知道setTimeout是否被递归调用?我们可以在jsperf或使用benchmark.js进行实验。
// async test
deferred.resolve()

这将导致Uncaught RangeError: Maximum call stack size exceeded.,这意味着一旦调用deferred.resolve,测试将在同一个tick/stack上再次运行。因此,在回调情况下,setTimeout在其自己的调用上下文中被调用并嵌套在另一个setTimeout中,这将将最小超时设置为4ms。
但在promise情况下,根据promise规范,.then回调在下一个tick之后被调用,而v8 不使用setTimeout调用回调函数。它使用类似于nodejs中的process.nextTicksetImmediate的东西,而不是setTimeout。这将重新设置setTimeout嵌套级别为0,并使setTimeout延迟为0ms。

1
好发现,也解释了为什么第一个值是1毫秒,其他值都是4毫秒。 - Esailija

3

首先,您的基准测试设计有误。它只会测量最小的 setTimeout 值,而不是回调函数和 Promise 之间的性能差异。

最小延迟是 4ms,因此结果不能超过每秒 250 次操作。不知何故,调用 new Promise 会消除最小的 4ms 延迟。

如果您想要测量 Promise 和回调函数之间的差异,您需要消除这种不自然的瓶颈。因此,您不仅要在并发级别为 1 的情况下进行测量,还要在每次调用之间等待 4ms。

JSPErf 并不容易设置并发性,但这里是并发性为 1000 的设置:

http://jsperf.com/promise-vs-callback/7


奇怪的是,在Chrome 32和33之间,结果不同。 - thefourtheye
我本来以为setTimeout的最小延迟也是4毫秒,但是我进行了一个快速测试以确保,我意识到它不是这样的:console.time(1); setTimeout(function(){console.timeEnd(1)},0);将显示1.3毫秒,这不再是4毫秒。在这种情况下,我并不真正关心测量promise与回调函数的区别,但我感兴趣的是v8使用什么黑魔法使promise的情况更快。这就是我的问题。 - Farid Nouri Neshat
@FaridNouriNeshat 这远远不足以测试最小延迟,请运行以下示例:var l = 500; var prev = Date.now(); setTimeout(function F(){ var now = Date.now(); console.log(now -prev); prev = now; if(l-- > 0) setTimeout(F, 0) }, 0); 只有前几个的值为1,而其余大多数为4和5。 - Esailija
@FaridNouriNeshat 有趣的事实是,这个人编写的库实际上比本机承诺更快:D - Benjamin Gruenbaum
另一个有趣的事实是,这个家伙编写的库实际上比我知道的任何其他管理回调和并发的东西都要快。 向强大的蓝鸟作者致敬 - Farid Nouri Neshat

1

正如Esailija所指出的那样,这与Promise中奇怪的setTimeout优化有关。 还可以看到使用更快的setTimeout替代方案制作的相同基准测试: http://jsperf.com/promise-vs-callback/8 它给出了更符合预期的结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接