0

I wrote two recursive functions that sum numbers from a array. They do the same thing, one asynchronously and the other synchronously. The async function took about 9x the time the sync one did.

Shouldn't the async function take advantage from the fact of running more tasks at the same time?

The functions

// Asynchronously sum the numbers in array
async function sumAsync(arr){

    if(arr.length == 1) return arr[0];

    const half = arr.length/2;

    // Numbers on the left half
    const left = arr.filter((e, i) => {
        return i < half;
    });

    // Numbers on the right half
    const right = arr.filter((e, i) => {
        return i >= half;
    });

    // Recursive call
    const leftR = sumAsync(left);
    const rightR = sumAsync(right);

    // Wait for resolves
    await Promise.all([leftR, rightR]);

    return await leftR + await rightR;
}


// Synchronously sum the numbers in array
function sumSync(arr){

    if(arr.length == 1) return arr[0];

    const half = arr.length/2;

    // Numbers on the left half
    const left = arr.filter((e, i) => {
        return i < half;
    });

    // Numbers on the right half
    const right = arr.filter((e, i) => {
        return i >= half;
    });

    // Recursive call
    const leftR = sumSync(left);
    const rightR = sumSync(right);

    return leftR + rightR;
}

Testing them

(async () => {

    const LENGTH = 1048576; // 1024^2
    const arr = Array.from(Array(LENGTH), num => Math.random()*10 | 0);
    //  arr[1048576] <- random (0 ~ 9)

    // Async sum
    console.log('ASYNC');
    before = Date.now();
    console.log(`SUM: ${await sumAsync(arr)}`);
    after = Date.now();
    console.log(`TIME: ${after - before} ms`);

    // Sync sum
    console.log('SYNC');
    before = Date.now();
    console.log(`SUM: ${sumSync(arr)}`);
    after = Date.now();
    console.log(`TIME: ${after - before} ms`);

})();

Results

// ASYNC
// SUM: 4720832
// TIME: 5554 ms

// SYNC
// SUM: 4720832
// TIME: 613 ms

2 Answers2

3

The return value of an async function is always a Promise, even if the function only carries out synchronous operations, and the await (or .then) of a Promise will only run what follows during a microtask (after the current synchronous code is finished running). With a large array, that'll result in a lot of unnecessary microtasks wrapping synchronous code.

When nothing actual asynchronous is going on, this is just extra baggage, and results in additional processing time and power required.

Shouldn't the async function take advantage from the fact of running more tasks at the same time?

Javascript is single-threaded, even with async functions. If multiple async functions are called at once, only one path through the code may be "active" at any one time. If the total processing time required for all tasks is, say, 1000 ms, in standard Javascript, there's no way around spending at least 1000 ms.

You're not actually running more tasks at the same time - you're just wrapping the tasks in Promises (unnecessarily), while doing the same work.

For truly parallel actions, you'll have to use something provided by your current environment, such as child_process in Node, or a web worker.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • What do you call a "*tick*" here? – Kaiido Feb 22 '20 at 02:12
  • I thought it'd be a more intuitive word than "microtask", something that doesn't run immediately, but after the current running code is done. It's also what Node calls something which is [nearly identical](https://stackoverflow.com/q/55467033) to a microtask. – CertainPerformance Feb 22 '20 at 02:23
  • It's unfortunately a bit misleading in my mind since "tick" is often used to describe a full iteration of the event loop, which it is not at all. The microtask will get ran **immediately** after the task where it got resolved ended. So if your task ends with this Promise resolving, it's actually ran synchronously. – Kaiido Feb 22 '20 at 02:29
  • 1
    Yeah, I see what you mean. Idea came from `process.nextTick(callback)`, but that's a really odd name for something that runs `callback` near immediately, *before* the next tick – CertainPerformance Feb 22 '20 at 03:03
2

Short version: async doesn't do more than one thing at a time. It switches between tasks (with overhead for each switch) in a queue, and when one task blocks, it hands control off to another (with overhead for the switch, and requeueing the blocked task when it unblocks).

Long version: Async doesn't mean parallel processing, it means interleaved (concurrent, cooperative) processing. JavaScript is still single-threaded even with async usage, and all of the actual work you do is purely CPU bound. In fact, your only real concurrency is that the async code will be scheduling, pausing and resuming your recursive calls repeatedly (but still only doing work for one at a time), while the sync code will just do them in order as fast as possible, with no event loop involvement.

The benefit of async code is that when blocking I/O (including stuff like waiting on user input) is being performed, that task can be suspended until it's unblocked by some out-of-band signal (I/O done, user clicked mouse, whatever), and other tasks can run. The benefit is in reaping the benefits of concurrent (but not parallel) processing in situations where most tasks, most of the time, are waiting for something, so the few that are ready to run can begin running immediately (and since they're usually not running, the overhead of task switching doesn't matter much; most of the time, there's nothing to switch to, so much of the overhead is paid when you've got nothing better to do). But it's definitely higher overhead than just doing number-crunching without a pause.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271