Live data from Hacker News

How Rust optimizes async/await

tmandry.gitlab.io

111–120 of 130 posts

Re: How Rust optimizes async/await

#111
post #86

Earlier quoted context omitted.

That version of Rust also did async I/O in the runtime. Async I/O has always been part of Rust. The model changed because there was too much overhead doing it the more ergonomic way and it got booted out of the runtime.

Yep, this is a great point. Someday, we should get a book about the history of Rust together...

I didn’t know about this.. I’d love to read that book :)

Re: How Rust optimizes async/await

#112

As a reminder, you don't need to use async/await to implement socket servers in Rust. You can use threads, and they scale quite well. M:N scheduling was found to be slower than 1:1 scheduling on Linux, which is why Rust doesn't use that solution. Async/await is a great feature for those who require performance beyond what threads (backed by either 1:1 or M:N implementations) can provide. One of the major reasons behi…

epoll io loop performs better for most network io though and is simplier to manage when you have to start dealing with out of band issues (like efficient hearbeats - every time I've had a conversation without how to move some of the heartbeat code over to async it comes down to just accepting it isn't going to be as efficient as my c++ implementation and either strain heavily of accept over publication). Last time I…

> tokio, wasn't very efficient at changing events in the queue (requiring a an extra self signal to get the job done).

Could you expand on that?

Re: How Rust optimizes async/await

#113
post #34

Earlier quoted context omitted.

In .NET something similar is possible via ValueTask.

ValueTask is more of an optimization for scenarios where the Future (or Task ) is often intended to be ready immediately (synchronously). For those cases its wasteful to first allocate a continuation on the heap, and then not to use it - because the code after the await can directly be executed. The introduction of ValueTask allowed C# code to only perform dynamic allocations whenever the task definitely needed to be…

Thanks for the thoughtful reply.

You are right, however there is a scenario that you have forgotten.

The possibility that the JIT might eventually optimise the code path given enough PGO data, and apply RVO or similar approaches.

Naturally I don't know if RyuJIT can already do this kind of optimization, given that only recently they have strengthened their focus on this area.

However this kind of optimizations are already doable in Graal, so it is possible that Microsoft also adds them to RyuJIT.

Which in any case would rule out some of the Rust deployment scenarios I guess.

Re: How Rust optimizes async/await

#114
post #37
post #35

Earlier quoted context omitted.

> Rust kind of skipped the whole non-blocking thing and went straight to tokio, then had to backtrack a little to mio+tokio, then when mio is only half way jupmed to async/await. Not sure exactly what you mean here; mio predates tokio by a couple years, and has been possible to use standalone since before tokio ever became a thing.

Early versions of mio had a bunch of non-essential things like a timer wheel built-in, that were later removed.

What does tokio have to do with that?

Re: How Rust optimizes async/await

#115
post #98

Earlier quoted context omitted.

For #1, there's a well-sourced Stack Overflow post https://softwareengineering.stackexchange.com/a/377514 .

Cool, I'll take a look! I was quickly searching around and found this paper [1]: "Pause 'n' play: formalizing asynchronous C#". It looks promising, although it is behind a paywall ;-(. Also, the keyword "formalizing" tells me that maybe this goes a bit deeper than the kind of description I'm looking for... 1: https://dl.acm.org/citation.cfm?id=2367181

sci-hub.tw usually helps with getting papers that are behind paywalls!

Re: How Rust optimizes async/await

#116

As a reminder, you don't need to use async/await to implement socket servers in Rust. You can use threads, and they scale quite well. M:N scheduling was found to be slower than 1:1 scheduling on Linux, which is why Rust doesn't use that solution. Async/await is a great feature for those who require performance beyond what threads (backed by either 1:1 or M:N implementations) can provide. One of the major reasons behi…

> One of the major reasons behind the superior performance of async/await futures relative to threads/goroutines/etc. is that async/await compiles to a state machine in the manner described in this post, so a stack is not needed.

That's a great optimization but doesn't that mean it also breaks stack traces?

Re: How Rust optimizes async/await

#118
post #80

As a reminder, you don't need to use async/await to implement socket servers in Rust. You can use threads, and they scale quite well. M:N scheduling was found to be slower than 1:1 scheduling on Linux, which is why Rust doesn't use that solution. Async/await is a great feature for those who require performance beyond what threads (backed by either 1:1 or M:N implementations) can provide. One of the major reasons behi…

I find async/await easier to reason about than threads for anything more involved than the 1 request per thread web server use case. This is because you avoid bringing in the abstraction of threads (or green threads) and their communication with one another. You trade syntactical complexity (what color is your function, etc), for semantic complexity (threads, channels, thread safety, lock races).

Async/await are effectively threads, the switches are just scheduled statically.

Re: How Rust optimizes async/await

#119

How does `yield` work under the hood? Does it add a reference to some array, with code to loop over all the references with some small timeout until the reference status changes from "pending" to "completed"?

It gets converted into a state machine. || { yield 2; yield 3; yield 5; } will get converted to a struct that implements the Generator trait[1] with a resume method something like fn resume(self: Pin ) -> GeneratorState { match self.next_state { 0 => { self.next_state = 1; Yielded(2) }, 1 => { self.next_state = 2; Yielded(3) }, 2 => { self.next_state = 3; Yielded(5) }, _ => Complete(()) } } Local variables become fie…

Just like for async, borrow across yield points here are a special feature that you couldn't implement in Rust as an open coded state machine, you'd have to find a workaround.

Re: How Rust optimizes async/await

#120

This is one of the most concise tutorials on how generators, coroutines and futures/promises are related (from first principles) that I've seen. I'm hopeful that eventually promises and async/await fade into history as a fad that turned out to be too unwieldy. I think that lightweight processes with no shared memory, connected by streams (the Erlang/Elixer, Go and Actor model) are the way to go. The advantage to usin…

> I think that lightweight processes with no shared memory, connected by streams [...] are the way to go.

No, at least not in general. There are a lot of problems in the real world for which "no shared memory" is incompatible with "efficient parallelism".

Post reply on HN