6

While testing the performance of await, I uncovered a confounding mystery. I ran each of the following code snippets several times each in the console to filter out flukes, and took the average times of the relevant data.

(function(console){
    "use strict";
    console.time();
    var O = [1];
    for (var i=0; i !== 107000; ++i) {
        const O_0 = O[0];
        O[0] = O_0;
    }
    console.timeEnd();
})(console);

Resulting in: default: 5.322021484375ms

Next, I tried adding making it asynchronous

(async function(console){
    "use strict";
    console.time();
    var O = [1];
    for (var i=0; i !== 107000; ++i) {
        const O_0 = O[0];
        O[0] = O_0;
    }
    console.timeEnd();
})(console);

Nice! Chrome knows its stuff. Very low overhead: default: 8.712890625ms

Next, I tried adding await.

(async function(console){
    "use strict";
    console.time();
    var O = [1];
    for (var i=0; i !== 107000; ++i) {
        const O_0 = O[0];
        O[0] = await O_0;
    }
    console.timeEnd();
})(console);

This results in 100x speed reduction: default: 724.706787109375ms

So, there must be some logical reason, right? I tried comparing the types prior.

(async function(console){
    "use strict";
    console.time();
    var O = [1];
    for (var i=0; i !== 107000; ++i) {
        const O_0 = O[0];
        O[0] = typeof O_0 === "object" ? await O_0 : O_0;
    }
    console.timeEnd();
})(console);

Okay, so that is not it: default: 6.7939453125ms

So then, it must be the promise-part: checking to see if the item passed to await is a promise. That must be the culprit, am I right or am I right?

(async function(console, Promise){
    "use strict";
    const isPromise = Promise.prototype.isPrototypeOf.bind(Promise);
    console.time();
    var O = [1];
    for (var i=0; i !== 107000; ++i) {
        const O_0 = O[0];
        O[0] = isPromise(O_0) ? await O_0 : O_0;
    }
    console.timeEnd();
})(console, Promise);

This results in: default: 7.2041015625ms

Okay, okay, let us give Chrome the benefit of the doubt. Let us assume, for a second, that they programmed await far less than perfectly.

(async function(console, Promise){
    "use strict";
    const isPromise = Promise.prototype.isPrototypeOf.bind(Promise.prototype);
    console.time();
    var O = [1];
    for (var i=0; i !== 107000; ++i) {
        const isAnObject = typeof O[0] === "object" ? true : false;
        const isThisAPromise = isPromise(O[0]);
        O[0] = isAnObject && isThisAPromise ? await O[0] : O[0];
    }
    console.timeEnd();
})(console, Promise);

But even this fails to explain the poor performance of await: default:7.85498046875ms

Okay, honestly, I give up. I would think that await would be at least 100x faster than it is now. I cannot think of a single good reason why it would not be 100x faster in a perfect world. However, we do not live in a perfect world, so there inlies the question: how? How? How is it this slow? Is there any hope of it being any faster in the future (like maybe, say, around about 100x faster)? I am looking for facts and an objective analysis of this issue that would explain the puzzling mystery I am seeing in the above performance tests.

Jack G
  • 4,553
  • 2
  • 41
  • 50
  • 8
    What is the puzzling mystery? Fulfilling promises takes time. The engine needs to allow other tasks to run. Without `await`, Chrome can concentrate on doing your task. With 107000 awaits, it's like trying to program while you have a chatty boss coming to bug you about every little thing all the damn time. – Amadan Sep 07 '18 at 11:45
  • 4
    I'm not sure what is the benefit of comparing promises to synchronous execution? Shouldn't you rather compare `async`/`await` with a `then` chain? – Bergi Sep 07 '18 at 11:49
  • 5
    Notice that `await` does not check whether the awaited value is a promise, and continues synchronously when it is not. It always does `Promise.resolve(value)` and waits for that, at least one tick. You shouldn't be surprised that doing `await` conditionally (and in your case, never, as none of your awaited values are promises) is as fast as not doing `await` at all. – Bergi Sep 07 '18 at 11:52
  • 3
    I mean, it might be flippant, but it's right there in the name: `await`. Who knew that waiting takes time. :) – Amadan Sep 07 '18 at 12:06
  • If `await` is passes a non-promise object, there should be no delay. – Jack G Sep 07 '18 at 13:00
  • 2
    @JackGiffin But there is a delay - any value is converted to a promise and then waited for, also allowing other concurrent promise chains to make progress. Just don't pass non-promise objects to `await` - as you demonstrated it is as fast as you expect! – Bergi Sep 07 '18 at 13:05

1 Answers1

10

You can easily observe a difference between an await expression and lack thereof. At the very least, you are asking the engine to look at the microtask queue, possibly doing other work that happened as a result of I/O completing. Given that, this cannot possibly be optimized into nothing.

If you truly wish to spin the CPU for a few milliseconds, don't write await.

Here's an example. It prints 1 2 3.

Promise.resolve().then(()=>console.log(2));

(async()=>{
  console.log(1);
  await undefined;
  console.log(3);
})();

await undefined is not a "do-nothing" statement. It's JavaScript's cooperative multitasking.

Josh Lee
  • 171,072
  • 38
  • 269
  • 275
  • 1
    Thank you so much. This key piece of knowledge explains everything. Thinking out loud, d@mn did W3 botch up the asynchrony spec. Let us assume one is in a circumstance where `await` might or might not be needed, but you cannot know for certain. In such a case, in order to prepare for `await`, one convert all dependent `function`s into `async function`s and `await` all return values from these functions. This would be just as fast as non-asynchronously if only `await`/`async` returned immediately when nothing asynchronous happens. If only Javascript asynchrony could be like Lua-coroutines. – Jack G Sep 08 '18 at 22:18
  • 3
    @JackGiffin When are you in circumstances to only sometimes need to do something asynchronous, and still do this stuff in a tight loop where performance would matter? – Bergi Sep 09 '18 at 10:16
  • 1
    I am attempting to write fully modular, fully scalable, fully extensible, and wicked fast code. I have got the layout down, but now my problem is asynchrony: how to pause the call stack on-demand if a certain extension in a certain area needs to be asynchronous (Example: needs to perform HTTP request before it can know what value to return). My best-solution thus far is to conditionally make the entire program asynchronous if one of the modules requires something to be asynchronous. – Jack G Sep 09 '18 at 17:25
  • 1
    `if (x instanceof Promise) x = await x;`. It waits on a promise, it doesn't wait on a non-promise. It will still need a function marked as `async`, though. – Amadan Sep 10 '18 at 12:18
  • 1
    https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise – Josh Lee Sep 10 '18 at 19:58