Issue
I refactored JavaScript code for Node.js (v10.13.0) which was previously synchronous into asynchronous code using async/await. What I noticed afterwards was a performance degradation of ~3x slower program execution time.
Is there a performance penalty when transforming a chain of synchronous function calls into asynchronous function calls?
Simplified Example
Changing synchronous code
function fn1() {
return 1;
}
function fn2() {
return fn1();
}
(function() {
const result = fn2();
});
into asynchronous code:
async function fn1() {
return 1;
}
async function fn2() {
return await fn1();
}
(async function() {
const result = await fn2();
})();
Is there any event-loop-magic which could make the latter code slower in a Node.js webapp?
Solution
Here’s a more advanced benchmark that calculates Fibonacci series with either a synchronous or async function:
async function benchmark(M = 1000000, N = 100) {
function fibonacci_sync(num) {
let a = 1, b = 0, temp
while (num >= 0) {
temp = a; a = a + b; b = temp; num--
}
return b
}
async function fibonacci_async(num) {
let a = 1, b = 0, temp
while (num >= 0) {
temp = a; a = a + b; b = temp; num--
}
return b
}
timeitSync ('sync', M, () => {for(let i = 0; i < N; i++) fibonacci_sync(i)})
await timeit('async', M, async () => {for(let i = 0; i < N; i++) await fibonacci_async(i)})
}
Sample execution times in node.js – async turns out to be 2.8x slower:
sync: 4.753s
async: 13.359s
With a larger M
, but smaller N = 10
instead of N=100 (shorter calculation, so awaits have bigger impact), the async function becomes 14.5x slower (ooops!!):
sync: 0.499s
async: 7.258s
This is on Node v16.13.1. The benchmark was inspired by this post:
https://madelinemiller.dev/blog/javascript-promise-overhead/
For the sake of completeness, here are the timeit
functions as used above:
async function timeit(label, repeat, fun) {
console.time(label)
for (let i = 0; i < repeat; i++) await fun()
console.timeEnd(label)
}
function timeitSync(label, repeat, fun) {
console.time(label)
for (let i = 0; i < repeat; i++) fun()
console.timeEnd(label)
}
When measuring fibonacci_sync
with async timeit
instead of timeitSync
, the execution time grows from 0.499s
to 1.2s
in the last example, which is another confirmation that async
brings a lot of slowdown.
So, yes, indeed, asynchronous calls may introduce a HUGE decline in performance, even of an order of magnitude. Each call must go through an event queue, whose management seems to create a substantial overhead. This definitely should be taken into account when implementing code that’s densely packed with async functions.
Given how "infectious" the async
paradigm is – with a single low-level function being async, all its callers up the tree must be async too – I’d be glad that JS introduces optimizations to allow await...
be executed instantly in some (most) cases rather than being pushed to a queue again and again every couple of instructions. This could benefit all the scenarios where await
is wrapped up in a conditional and rarely needs to actually stop the function, but still requires the function be declared as "async", no matter how often the "await" is being reached.
Answered By – Marcin Wojnarski
Answer Checked By – Timothy Miller (BugsFixing Admin)