[SOLVED] Is there a Performance Difference of Non-Parallel Async Loop vs Sync Loop in Node.js?

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)

Leave a Reply

Your email address will not be published. Required fields are marked *