I've felt before that compilers often don't put much effort into optimizing the "trivial" cases.
Overly dramatic title for the content, though. I would have clicked "Async Rust Optimizations the Compiler Still Misses" too you know
I've felt before that compilers often don't put much effort into optimizing the "trivial" cases.
Overly dramatic title for the content, though. I would have clicked "Async Rust Optimizations the Compiler Still Misses" too you know
Yes, we can have async in traits and closures now. But those are updates to the typesystem, not to the async machinery itself. Wakers are a little bit easier to work with, but that's an update to std/core.
As I understand it, the people who landed async Rust were quite burnt out and got less active and no one has picked up the torch. (Though there's 1 PR open from some google folk that will optimize how captured variables are laid out in memory, which is really nice to have) Since I and the people I work with are heavy async users, I think it's maybe up to me to do it or at least start it. Free as in puppy I guess.
So yeah, the title is a little baitey, but I do stand behind it.
Great to see people wanting to get involved with the project, though. That’s the beauty of open source: if it aggravates you, you can fix it.
Retrospectively, i think everyone is satisfied with the adopted syntax.
Maybe it’s a case of agree and commit, since it can’t really be walked back.
await using db = await sqlite.connect(await ctx.getConfig("DB_CONN"));
It's not so bad when you have one `await foo` vs `foo.await`, it's when you have several of them on a line in different scopes/contexts.Another one I've seen a lot is...
const v = await (await fetch(...)).json();
Though that could also be... const v = await fetch(...).then(r => r.json());
In any case, it still gets ugly very quickly.```dart (await taskA()).doSomething() (await taskB()) + 1 (await taskC()) as int ```
vs.
```rust taskA().await.doSomething() taskB().await + 1 taskC().await as i32 ```
It gets worse if you try to compose:
```dart (await taskA( (await taskB( (await taskC()) as int )) + 1) ).doSomething() ```
This often leads to trading the await syntax for `then`:
```dart await taskC() .then((r) => r as i32) .then(taskB) .then((r) => r + 1) .then(taskA) .then((r) => r.doSomething()) ```
But this is effectively trading the await structured syntax for a callback one. In Rust, we can write it as this:
```rust taskA(taskB(taskC().await as i32).await + 1).await.doSomething() ```
This is a code block
HN has never used markdown so the triple-tick does nothing but create noise here.I was worried about features that I still don't love like `.match` etc (I'm more open to these now).
Post-fix macros would have been very complex. Scoping alone is complex.
`.await` kinda just works. It does everything you want and the one cost is that it looks like a property access but it isn't. A trivial cost in retrospect that I was a huge baby about, and I'll always feel bad about that.
Postfix macros had some very tricky issues and it would have delayed things a lot to figure out the right resolution.
One problem I have with systems like gevent is that it can make it much harder to look at some code and figure out what execution model it's going to run with. Early Rust actually did have a N:M threading model as part of its runtime, but it was dropped.
I think one thing Rust could do to make async feel less like an MVP is to ship a default executor, much like it has a default allocator.
By providing a default, I think you're going to paint yourself into a corner. Maybe have one of two opt-in executors in the box... one that is higher resource like tokio and one that is meant for lower resource environments (like embedded).
As an Elixir + Erlang developer I agree it’s a great programming model for many applications, it just wasn’t right for the Rust stdlib.
You may also need to setup a large stack frame for each C FFI call.
Claim-2: async versus sync is a fundamental division in CS
Discuss amongst yourselves. I lean towards thinking both are probably true (P~70%, P~90%)
See: “What Color is Your Function?” by Bob Nystrom (2015). https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
Hi. The article calls Rust async an MVP. You should expect strong reactions when you frame it like that.
"MVP" has a generally understood meaning; distorting that is unhelpful and confusing. Rust's async was not an MVP when it was released in 2019. It was the result of a lot of earlier work.
Rust async: (a) works well for a lot of people and orgs in production settings and (b) is arguably better designed than most (all?) other async implementations. Calling it an MVP is far from "simply the truth". It is an opinion -- and frankly a pretty clickbaity one. I appreciate your article's attention to detail, but the title is straight up shameful sensationalism.
I strive to not reflexively defend the status quo, but I get really chafed when people conveniently blur the difference between fact and opinion.
Please argue on narrowest correct claims available. The current title overstates your claims and undermines its overall credibility. Your central claim (as I read it) is that for embedded software there are opportunities for async improvement in Rust. Yeah this might sound boring, but I think it's accurate.
My other main criticism of your article is when it claims Rust async breaks the "zero cost abstraction" principle. I don't buy this claim, because you do not show that hand rolling the code provides the same guarantees. A lot of people misunderstand what "zero cost" means; your article wouldn't be the first to give the wrong impression.
Writing is hard (different audiences bring different backgrounds), and I commend anyone who puts their ideas out into the world. Please take this as constructive feedback: please agree or disagree with me on the merits. Ask and engage where I'm unclear.
The team literally described it as such.
One of the main architects of Rust’s async/await, withoutboats, left a comment on lobsters:
> It's just the truth. Neither in the language design nor in the compiler implementation has hardly any progress been made in the now 7 years since we shipped the MVP. The people primarily involved in delivering the MVP all become less active in the project around the same time and delivery since then has stalled out.
>
> I hope this person receives the support to do this work.
Rust’s async is great, and I feel you around some of the less informed criticisms. But it’s been called an MVP for a decade now, it’s not an insulting characterization. Just because it’s been an MVP does not mean it’s not good or useful.
Even if MVP is the correct term for its current state, it has a connotation to it which less informed folks will take away the wrong meaning from, so perhaps it's not useful to continue to propagate it even if true.
This is both true and not true. It's no secret that the async ecosystem has had deep social rifts for a very long time, and that's made it very tough to actually make progress by anyone, regardless of the desire to.
It is true that async massively gave a boost to Rust's adoption, and it is good enough for many users. It is a monumental technical achievement. At the same time, that doesn't mean it's perfect.
> On this coming Thursday, November 7, async-await syntax hits stable Rust, as part of the 1.39.0 release. This work has been a long time in development -- the key ideas for zero-cost futures, for example, were first proposed by Aaron Turon and Alex Crichton in 2016! -- and we are very proud of the end result. We believe that Async I/O is going to be an increasingly important part of Rust's story.
> While this first release of "async-await" is a momentous event, it's also only the beginning. The current support for async-await marks a kind of "Minimum Viable Product" (MVP). We expect to be polishing, improving, and extending it for some time.
> Already, in the time since async-await hit beta, we've made a lot of great progress, including making some key diagnostic improvements that help to make async-await errors far more approachable. To get involved in that work, check out the Async Foundations Working Group; if nothing else, you can help us by filing bugs about polish issues or by nominating those bugs that are bothering you the most, to help direct our efforts.
---
MVP originated from the lean startup world: the core meaning is to build the smallest thing that validates demand. Async-await (a-a) was so much more than that.
Whoever wrote the blog post wrote "MVP" which leaves a reader wondering: are those scare quotes? [2]
Nice a-a was "in the air" since Go shipped it in 2009. Compiled state machines hit the scene not long after: C# in 2012, Clojure in 2013. So demand did not need validation. As I understand it, probably the biggest driver for Rust a-a was to provide a common foundation for work going forward.
The effort and thinking that went into async/await probably blows away 99.9% of MVPs released out into the world! And I don't think it would be crazy to say Rust's a-a was better than ~80% of even "1.0" products.
I don't want to get bogged down in mere definitions... I'm emphasizing the primary emotional vibe. Using MVP (or "MVP") is a surefire way to conjure the wrong emotional valence. It invites confusion and downplays years of incredible work.
My feeling is that original blog post used that framing because the Rust team is famously open to taking however much time is needed to get things as close to perfect as they know how.
[1]: https://blog.rust-lang.org/2019/11/07/Async-await-stable/
The author seems to be obsessing about the overhead for trivial functions. He's bothered by overhead for states for "panicked" and "returned". That's not a big problem. Most useful async blocks are big enough that the overhead for the error cases disappears.
He may have a point about lack of inlining. But what tends to limit capacity for large numbers of activities is the state space required per activity.
Is it really though?
In my experience many Rust applications/libraries can be quite heavy on the indirection. One of the points from the article is that contrary to sync Rust, in async Rust each indirection has a runtime cost. Example from the article:
async fn bar(blah: SomeType) -> OtherType {
foo(blah).await
}
I would naively expect the above to be a 'free' indirection, paying only a compile-time cost for the compiler to inline the code. But after reading the article I understand this is not true, and it has a runtime cost as well.Most useful async blocks are deeply nested, so the overhead compounds rapidly. Check the size of futures in a decently large Tokio codebase sometime
not just too dramatic
given that all the things they list are
non essential optimizations,
and some fall under "micro optimizations I wouldn't be sure rust even wants",
and given how far the current async is away from it's old MVP state,
it's more like outright dishonest then overly dramatic
like the kind of click bait which is saying the author does cares neither about respecting the reader nor cares about honest communication, which for someone wanting to do open source contributions is kinda ... not so clever
through in general I agree rust should have more HIR/MIR optimizations, at least in release mode. E.g. its very common that a async function is not pub and in all places directly awaited (or other wise can be proven to only be called once), in that case neither `Returned` nor `Panicked` is needed, as it can't be called again after either. Similar `Unresumed` is not needed either as you can directly call the code up to the first await (and with such a transform their points about "inlining" and "asyncfns without await still having a state machine" would also "just go away"TM, at least in some places.). Similar the whole `.map_or(a,b)` family of functions is IMHO a anti-pattern, introducing more function with unclear operator ordering and removal of the signaling `unwrap_` and no benefits outside of minimal shortening a `.map(b).unwrap_or(a)` and some micro opt. is ... not productive on a already complicated language. Instead guaranteed optimizations for the kind of patterns a `.map(b).unwrap_or(a)` inline to would be much better.
Depends somewhat on your expectations, I suppose. Compared to Python, Java, sure, but Rust off course strives to offer "zero-cost" high level concepts.
I think the critique is in the same realm of C++'s std::function. Convenience, sure, but far from zero-cost.
This may look like a case of over-optimization, but given how many times i've seen this pattern, i assume it builds up to a lot of unnecessary fluff in huge codebases. To be clear, in that case, the concern is not really about runtime speed (which is super fast), but rather about code bloat for compilation time and binary size.