Async functions and codegen

Does making a function async push all the codegen to the crate calling the async function?

Background
I have a big complicated function in a crate:

// crate a
pub async fn do_lots_of_async_stuff(usize) -> usize

And then this function is called in lots of dependent crates, for example crate b:

// crate b
...
    // If the next line is commented out the compilation time for crate b decreases by 10s
    let i = do_lots_of_async_stuff(i).await;
...

So to me it seems as if a async function is not compiled once in the create wherre it is defined but leads to a whole lot of codegen in all dependent crates (in my case many).

Can this be correct? If so, is there any preferred way to work around it?

Regards

It really does seems so, in my mind a bit absurd, in my workspace I now save 40s+ of compilation time (on a 32c machine) by changing a single function in a single crate from:

pub async fn calc(...) -> usize

to

pub fn calc (...) -> BoxFuture<usize> {
   (async move { calc_inner(...).await }).in_current_span().boxed()
}

Does anyone know if this is "in line with expectation" and the preferred way to fix such problems?

yeah a callstsck of async fuctions get united ìnto a single state machine, this is probably a good thing in a lot of contexts because it allows to highly optimize them, if you want a future to reuse the same code then you need to make an explicit future type and do my_function()->MyFuture instead of async my_function()

2 Likes

Right, thanks for confirming this.

That might be ovious to some (that I should have returned a boxed future in the first place), but in "most" async usage context (web related stuff?) the trade of between compilation time and (in those contexts) unmeasurable performance impact (as well as more verbose code) seems a bit off :slight_smile:

doesn't necessary need to be boxed, just giving it a specific type should mean the codegen of that type gets reused in most places, using boxed future does speed up codegen further because it allows the caller of the function to fully ignore what the codegen of the future is

If there are no measurable performance impact then why are you using async in the first place?

Right, noted, but in practice boxed is the only alternative unless I want to create a Future type myself?

Sorry I didnt get that?

I have a huge benefit from using async (lots of io), but the performance between returning "state machine" async fn future and a boxed future is none (dwarfed by http overhead)

Is very simple quesion: async is an enormous complication on top of simple “one thread with a small stack per request” model. Yet you claim that you don't get a measurable impact. Why complicate your life with async, then?

Note: the answer “it's because there are no non-async web frameworks” is perfectly valid and “box everything” is the great way to go from there: you admit that you don't need async, but you couldn't avoid it thus you use the best approach, that you could… but that shouldn't be the default, now, should it?

If everyone doesn't care about async and everyone only uses it because others are using it… then maybe it's time to stop?

Have you actually measured that? Google serves billions of users without async… and if that's too slow for you then it's not clear why you are so sure that difference between async fn and boxed future doesn't slow you down enough to care.

Hi,

100% agree with most of you said, and I have actually looked for a non-async alternative webserver framework in Rust - but seems as if there is no such active project? (including database drivers) - they all seem to focus on async.

The "benefit" I mostly get from async thus is:

  • lots of functionality pre-exists (axum, db drivers etc.)
  • "simpler code" to span lots of tasks and wait for them - but agreed that this could from a performance perspective be ok with threads.

I.e. the key "problem" is that the community seems to have convered on suing async in this context.

But Iäm getting out of hand, it was not meant as a critique of other proples work - mostly my surprise at it.

The real problem is that currently it's not possible to write code that would work both in sync and async contexts.

Well… surprise is justified. The story of async in Rust is really awful (even if it's getting better), but the gist of the story, as I understand it is:

  1. Some people do care about async and memory allocations. And they need to care about both because bar where async is better than ”millions of threads” (like what Google did) is very-very low.
  2. Because it's easy to add memory allocations and but hard to remove memory allocations the default is to not add them.
  3. Ergo: we have a situation where thing that most developers want is not a default.

There are actually discussions and proposals to make this easier… but as I have said — this is where we are.

1 Like