3

Here's the jsperf: http://jsperf.com/promise-vs-callback

callback case (211 Ops/s):

// async test
var d = deferred;

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

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

Promise case(614 ops/s):

// async test
var d = deferred;

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

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

As you see promise are way faster, but they have more code. The question is why this happens.

Here deferred is to defined by jsperf to show it as the completion of the async test.

Farid Nouri Neshat
  • 29,438
  • 6
  • 74
  • 115
  • Man, since when does "more code" mean "less performance"? Sometimes you have to write a lot of code to make sure something is behaving well accoeding to the specifics of the underlying mechanism, to achieve better performance. – daniel.gindi Nov 09 '15 at 06:41

3 Answers3

6

As it seems the magic trick lies with in how chrome sets the minimum delay for setTimeout(fn, 0).

I searched for it and I found this: https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/Hn3GxRLXmR0/XP9xcY_gBPQJ

I quote the important part:

The way timer clamping works is every task has an associated timer nesting level. If the task originates from a setTimeout() or setInterval() call, the nesting level is one greater than the nesting level of the task that invoked setTimeout() or the task of the most recent iteration of that setInterval(), otherwise it's zero. The 4ms clamp only applies once the nesting level is 4 or higher. Timers set within the context of an event handler, animation callback, or a timer that isn't deeply nested are not subject to the clamping.

In the callback case, setTimeout is called recursively, in a context of another setTimeout , so the minimum timeout is 4ms. In the promise case, setTimeout is actually not called recursively, so the minimum timeout is 0(It wouldn't be actually 0, because other stuff has to run too).

So how do we know setTimeout is called recursively? well we can just conduct an experiment in jsperf or just using benchmark.js:

// async test
deferred.resolve()

Which will result in Uncaught RangeError: Maximum call stack size exceeded. Which means, once deferred.resolve is called, the test is run again on the same tick/stack. So in the callback case setTimeout is called in it's own calling context and nested in another setTimeout, which will set the minimum timeout to 4ms.

But in the promise case, .then callback is called after the next tick according to promise spec, and v8 doesn't use setTimeout calling the callback after the next tick. It uses something that must be similar to process.nextTick in nodejs or setImmediate, and not setTimeout. Which resets the setTimeout nesting level to 0 again and makes the setTimeout delay 0ms.

Farid Nouri Neshat
  • 29,438
  • 6
  • 74
  • 115
3

First of all, your benchmark is designed wrong. It is only going to measure the minimal setTimeout value, not the perf difference between callbacks and promises.

The minimal delay is 4ms so the result cannot be more than 250 operations per second. Somehow calling new Promise is removing the minimal 4ms delay.

If you wanted to measure promise and callback difference, you need to remove such unnatural bottlenecks. So not only are you measuring at concurrency level of 1, you are waiting 4ms between each call.

JSPErf doesn't make it easy to set concurrency, but here is with concurrency = 1000:

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

Esailija
  • 138,174
  • 23
  • 272
  • 326
  • Strange, results differ between Chrome 32 and 33 – thefourtheye Mar 31 '14 at 09:29
  • Well I expected the minimal delay of setTimeout be 4ms too, but then I did a quick test to make sure and I realized it's not: `console.time(1); setTimeout(function () { console.timeEnd(1) }, 0);` will show 1.3ms, which is not 4ms anymore. I'm not really interested in measuring promise vs callbacks in this case, but what I'm interested is that what black magic v8 does to make the promise case way faster. That's my question. – Farid Nouri Neshat Mar 31 '14 at 09:51
  • @FaridNouriNeshat that is not sufficient at all to test the minimal delay, run this for example: `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);` Only the first few have a value of 1, while the rest is mostly 4 and 5. – Esailija Mar 31 '14 at 10:02
  • @FaridNouriNeshat fun fact, the library this guy wrote actually _is_ faster than native promises :D – Benjamin Gruenbaum Mar 31 '14 at 15:37
  • Another fun fact, the library this guy wrote is actually faster than anything else that I know to manage callbacks and concurrency. *bow of respect to the author of mighty bluebird* – Farid Nouri Neshat Apr 02 '14 at 04:13
1

As Esailija pointed it's related to weird setTimeout optimizaition in Promise. See also same benchmark made with faster setTimeout alternative: http://jsperf.com/promise-vs-callback/8 it gives more expected results

Mariusz Nowak
  • 32,050
  • 5
  • 35
  • 37