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?
-
The recursive function (
fibonacci_10()
) is slower than the normal non-recursive function (fibonacci_20()
). -
Adding the async keyword to any function will add some overheads to the call. So, you should use the async keyword when necessary. Otherwise, using a normal function will be best.
-
Calling an async recursive function (
fibonacci_async_10()
) requires more memory (about 568KB from the above summary). If you have this kind of function in a web application environment, you might have to watch out for the memory consumption when the number of concurrent access increases. -
Returning Promise by a function (
fibonacci_promise()
) seems to save a little bit of memory as compare to 'automatically' returning a Promise by async function (fibonacci_async_20()
).
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
- Faster async functions and promises - https://v8.dev/blog/fast-async
- There was a discussion on why
better-sqlite3
package provides synchronous API instead of async function - https://github.com/WiseLibs/better-sqlite3/issues/262 - Here's another interesting discussion - https://snyk.io/blog/nodejs-how-even-quick-async-functions-can-block-the-event-loop-starve-io/
- Node.js event loop explains - https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick
Related posts
Jump to #JAVASCRIPT blog
Author
Lau Hon Wan, software developer.