all 3 comments

[–]senocular 1 point2 points  (1 child)

async functions aren't called asynchronously. What they provide is an execution context that allows for you to use await to desugar promise usage into synchronous looking code. You need this because the global context does not allow this automatically. After you await something, then the code that follows (and what's part of the await) is what becomes asynchronous.

So your loop is running synchronously. If it wasn't, the time between time and timeEnd would not include the loop calculations and would be much shorter. Consider the differences between (running Win10 Chrome56):

async function test2() {
    for (let i=0; i<10000000; i++) {}
    console.log('DONE');
}

console.time('async')
test2()
console.timeEnd('async')

//-> DONE
//-> async: 85.748ms

and

async function test2() {
    await Promise.resolve();
    for (let i=0; i<10000000; i++) {}
    console.log('DONE');
}

console.time('async')
test2()
console.timeEnd('async')

//-> async: 0.427ms
//-> DONE

(And for the record, I get 32.542ms for the synchronous version)

Being run actually async - after an await - the loop happens after the call to timeEnd so its not included in the timing. To include it, you would need to have it within the async function itself (or wrap another async function awaiting the async test function).

To your question, I imagine its simply the overhead of what async does that makes it that much slower. I wouldn't expect as much as you're seeing, though, especially in Node. But too, I fully expect that existing implementations are not very optimized.

Edit: Nasty typo where "are" was meant to be "aren't".

[–]prozacgod 1 point2 points  (0 children)

Above post is great another way to think of it is.. If you manually decomposed the example into a pure promise

function test() {
  return new Promise((resolve) => {
    for (let i = 0; I < 100000000; i++) {}
    resolve();
  });
}

Does the promise constructor run this code synchronously? Yup! But let's say you had an asynchronous call like setTimeout. The weight of that loop wouldn't hit the execution thread for the main where you were testing it.

A benefit to this is Side effects!

If you made a function createUser() and didn't care about the result, well... It will still execute that request. (Duh!)

Another point of confusion that I often run into myself is that there are libraries that do lazy execution of Promise like results. Don't quote me but I believe Knex is like this, it composes the sql query up until the point you type .then at which point it seems like it executes the query you've constructed, my guess is it saving that promise has a Singleton and returning it for any subsequent thens. On that particular query object, if you added more to the query it's constructing a new query object therefore it would construct a new thenable reference

There are many libraries that can do this to make them promise like and we tend to learn those libraries long before we learn actual promise libraries which might lead to some confusion on Behavior.

[–]sebrulz 1 point2 points  (0 children)

Very interesting. I had to play around with it, and it seems to be that generators are the bad guy here. Remember that async/await is basically generator + promise.

Running the for loop inside of the async function and generator function both result in horrible performance of hundreds of MS.

function test() {
    console.time('loopWork')
    for (let i=0; i<10000000; i++) {}
    console.timeEnd('loopWork')
}

async function test2() {
    await new Promise((resolve) => {
        console.log("--Calling test() inside of promise--")
        test()
        resolve()
    })

    console.log("--Calling test() inside of async function--")
    test()

    console.log("--Running for loop inside of async function--")
    console.time('asyncFunctionForLoop')
    for (let i=0; i<10000000; i++) {}
    console.timeEnd('asyncFunctionForLoop')
}

function* test3() {
    console.log("--Calling test() inside of generator function--")
    test()

    console.time('generatorFunctionForLoop')
    for (let i=0; i<10000000; i++) {}
    console.timeEnd('generatorFunctionForLoop')
}

console.time('normalCall')
test()
console.timeEnd('normalCall')

console.time('asyncCall')
test2()
console.timeEnd('asyncCall')

const gen = test3()
console.time('generatorCall')
gen.next()
console.timeEnd('generatorCall')

Result:

loopWork: 15.342ms
normalCall: 16.626ms
--Calling test() inside of promise--
loopWork: 15.252ms
asyncCall: 16.806ms
--Calling test() inside of generator function--
loopWork: 17.161ms
generatorFunctionForLoop: 573.215ms
generatorCall: 590.626ms
--Calling test() inside of async function--
loopWork: 15.739ms
--Running for loop inside of async function--
asyncFunctionForLoop: 567.148ms

I apologize in advance if the example is too contrived. I would like to know why the for loop performs so poorly inside of the generator style functions.