Welcome to ciysys blog

Async performance

Published on: 13th April 2023

Overview

Have you ever wondered what is the performance of using async/await keywords over the normal function? Is it ok just to make all functions to be the async version so that every line must start with await and make it more consistent for the reader? I'm really curious about it.

Summary of the async performance

Long story short: the following output was generated with Node.js v18.15.1. Please take note that you may not get the same output as shown below and this summary is just an explanation on how V8 JavaScript engine works.

Repeat the test for 5 times and the summary below is the average value.

Test recursive calls
  normal proc: 0.2305ms, memory usage: 1KB
  using await proc: 59.0970ms, memory usage: 568KB
  var %: 85.1287

Test non-recursive
  normal proc: 0.0199ms, memory usage: 1KB
  using await proc: 0.6576ms, memory usage: 26KB
  var %: 10.6817

Test non-recursive using Promise
  normal proc: 0.0191ms, memory usage: 0KB
  using await proc: 0.6474ms, memory usage: 16KB
  var %: 10.9651

What does the summary tell us?

Conclusion

This test gives us a basic idea on what is the extra time and memory to use the async function. But it does not mean that async function is bad because the normal function might block the event loop if the WHILE/FOR loop is taking too much CPU time. So, you may still have to find the balance between the uses of the async function (from concurrency point of view) and normal function (from speed point of view).

Here's the code

You may copy the following code and test it out on your computer.

const TEST_COUNT = 5;

//------------------------------------------------------------------------------
// Recursive version.
function fibonacci_10(num) {
    if (num <= 1) {
        return 1;
    }
    
    return fibonacci_10(num - 1) + fibonacci_10(num - 2);
}

//------------------------------------------------------------------------------
// With WHILE loop.
function fibonacci_20(num) {
    let a = 1;
    let b = 0;
    let temp;

    while (num >= 0) {
        temp = a;
        a = a + b;
        b = temp;
        num--;
    }
    return b;
}

//-------------------------
// Recursive version with async.
async function fibonacci_async_10(num) {
    if (num <= 1) {
        return 1;
    }

    return (await fibonacci_async_10(num - 1)) + (await fibonacci_async_10(num - 2));
}

//------------------------------------------------------------------------------
// async with WHILE loop.
async function fibonacci_async_20(num) {
    let a = 1;
    let b = 0;
    let temp;

    while (num >= 0) {
        temp = a;
        a = a + b;
        b = temp;
        num--;
    }
    return b;
}

//------------------------------------------------------------------------------
// Promise case.
function fibonacci_promise(num) {
    return new Promise((resolve, reject) => {
        let a = 1;
        let b = 0;
        let temp;
        
        while (num >= 0) {
            temp = a;
            a = a + b;
            b = temp;
            num--;
        }
        resolve(b);
    });
}

//------------------------------------------------------------------------------
async function test_recursive_version() {    
    let hrstart, hrend, mem11, mem12, mem21, mem22, x1, x2;
    
    // warm up the api.
    hrstart = process.hrtime();
    hrend = process.hrtime(hrstart);
       
    hrstart = process.hrtime();
    mem11 = process.memoryUsage.rss();

    for (let i = 0; i < 10; i++) {
        fibonacci_10(i);
    }
    
    mem12 = process.memoryUsage.rss();
    hrend = process.hrtime(hrstart);
    x1 = hrend[1] / 1000000;

    //-------------------------
    hrstart = process.hrtime();
    mem21 = process.memoryUsage.rss();

    for (let i = 0; i < 10; i++) {
        await fibonacci_async_10(i);
    }

    mem22 = process.memoryUsage.rss();
    hrend = process.hrtime(hrstart);
    x2 = hrend[1] / 1000000;

    return {
        normal: {
            duration: x1,
            mem: (mem12 - mem11) / 1024
        },
        async_ver: {
            duration: x2,
            mem: (mem22 - mem21) / 1024
        },
    };
}

//------------------------------------------------------------------------------
async function test_non_recursive_version() {
    let hrstart, hrend, mem11, mem12, mem21, mem22, x1, x2;
    
    hrstart = process.hrtime();
    mem11 = process.memoryUsage.rss();

    for (let i = 0; i < 10; i++) {
        fibonacci_20(i);
    }

    mem12 = process.memoryUsage.rss();
    hrend = process.hrtime(hrstart);
    x1 = hrend[1] / 1000000;

    //-------------------------
    hrstart = process.hrtime();
    mem21 = process.memoryUsage.rss();
    
    for (let i = 0; i < 10; i++) {
        await fibonacci_async_20(i);
    }

    mem22 = process.memoryUsage.rss();
    hrend = process.hrtime(hrstart);
    x2 = hrend[1] / 1000000;

    return {
        normal: {
            duration: x1,
            mem: (mem12 - mem11) / 1024
        },
        async_ver: {
            duration: x2,
            mem: (mem22 - mem21) / 1024
        },
    };
}

//------------------------------------------------------------------------------
async function test_promise_version() {
    let hrstart, hrend, mem11, mem12, mem21, mem22, x1, x2;
    
    hrstart = process.hrtime();
    mem11 = process.memoryUsage.rss();

    for (let i = 0; i < 10; i++) {
        fibonacci_20(i);
    }
    
    mem12 = process.memoryUsage.rss();
    hrend = process.hrtime(hrstart);
    x1 = hrend[1] / 1000000;

    //-------------------------
    hrstart = process.hrtime();
    mem21 = process.memoryUsage.rss();

    for (let i = 0; i < 10; i++) {
        await fibonacci_promise(i);
    }

    mem22 = process.memoryUsage.rss();
    hrend = process.hrtime(hrstart);
    x2 = hrend[1] / 1000000;
    
    return {
        normal: {
            duration: x1,
            mem: (mem12 - mem11) / 1024
        },
        async_ver: {
            duration: x2,
            mem: (mem22 - mem21) / 1024
        },
    };
}

//------------------------------------------------------------------------------
function summarize_result(result){
    let avg_result = {
        normal: {
            duration: 0,
            mem: 0
        },
        async_ver: {
            duration: 0,
            mem: 0
        },        
    };
    
    result.forEach((o2) => {
        avg_result.normal.duration += o2.normal.duration;
        avg_result.normal.mem += o2.normal.mem;

        avg_result.async_ver.duration += o2.async_ver.duration;
        avg_result.async_ver.mem += o2.async_ver.mem;
    });

    avg_result.duration = (((avg_result.async_ver.duration - avg_result.normal.duration ) / avg_result.normal.duration) / 3).toFixed(4);
    
    let len = result.length;
    console.log(`normal proc: ${avg_result.normal.duration.toFixed(4)}ms, memory usage: ${(avg_result.normal.mem / len).toFixed(0)}KB`);
    console.log(`using await proc: ${avg_result.async_ver.duration.toFixed(4)}ms, memory usage: ${(avg_result.async_ver.mem / len).toFixed(0)}KB`);
    console.log('var %:', avg_result.duration);
}


//==============================================================================

(async function () {
    let result;

    console.log(`Repeat the test for ${TEST_COUNT} times and the summary below is the average value.`);
    console.log('');

    console.group('Test recursive calls');    
    result = [];
    
    for (let i = 0; i < TEST_COUNT + 1; i++) {
        result.push(await test_recursive_version());
    }

    // remove the 'cold' start function calls which included JIT time.
    result.splice(0, 1);

    summarize_result(result);
    console.groupEnd();
    console.log('');

    //-------------------------
    // we delay the following tests so that GC (garbage collector) can do some cleanup.
    // As a result, the memory usage is more accurate.
    setTimeout(async () => {        
        console.group('Test non-recursive');
        result = [];

        for (let i = 0; i < TEST_COUNT + 1; i++) {
            result.push(await test_non_recursive_version());
        }
        
        // remove the 'cold' start function calls which included JIT time.
        result.splice(0, 1);

        summarize_result(result);
        console.groupEnd();
        console.log('');
    }, 500);

    //-------------------------
    // we delay the following tests so that GC (garbage collector) can do some cleanup.
    // As a result, the memory usage is more accurate.
    setTimeout(async () => {
        console.group('Test non-recursive using Promise');
        result = [];
        
        for (let i = 0; i < TEST_COUNT + 1; i++) {
            result.push(await test_promise_version());
        }
        
        // remove the 'cold' start function calls which included JIT time.
        result.splice(0, 1);
        
        summarize_result(result);
        console.groupEnd();
    }, 1000);

})();

References

Related posts

Jump to #JAVASCRIPT blog

Author

Lau Hon Wan, software developer.