I don't see how rust will manage to fix async await, unless making a v2 of the language. While working with sync rust is often a very pleasant experience, everytime I used async await was awful.
To the point that I avoid using rust for web /networking stuff, any other option is better.
It's also very discouraging to read some people telling there's no issue with async await. Of course if you use a thousands of libraries (that are more or less hacks), and never use trait and generics, it's probably a decent experience half of the time I'd say.
It's something I've only encountered with rust community, in Java when a feature sucks, we just say it sucks. Even the architect that approved it will say it sucks.
I recently built a little Rust project, which I wanted to integrate into a Web App using Web-assembly (wasm-binden). Writing the Rust code was super fun, however, when I execute one of my functions (which takes around 10 seconds to complete), the whole Web App freezes. Well, this should be an easy fix, just make the Rust function async - right? Nope. Making the Rust function async to return a Promise: Web App freezes. Wrapping the call of the Rust code in Promises: Web App freezes. Using Web Workers to run any Rust code in a separate thread: Great, now you can’t share ANY state whatsoever with the UI thread (and no, you can’t pass any Rust objects to another Web Worker thread, because any wasm objects only consist of _binding_ which need an initialised WASM instance in their own thread. To fix this problem, people on GitHub redirected me to a parallel retracer example, which requires nightly Rust and some other tacky tricks to get running. Maybe I will figure out how to integrate this into my project, maybe I won’t. All I wanted to do, is to run one simple async function using WASM. But as it turned out I will have to implement a frikin raytracer first.
Yeah you'd have the same issue if you ran the expensive computation in Javascript. What you need is worker threads, and yes, you can't share objects with worker threads easily.
The core problem I see is Rust tried too hard to avoid allocations/copies and a heavy async runtime. The result is confusing and difficult types and very unclear errors due to apis producing ridiculous types like Future<Arc<Box<dyn Thing>>>.
The language should have defined a consistent async model (like coroutines with message passing) and kept "raw async" confined to high performance special libraries and embedded use cases.
That's not Rust's fault but library creators / programmers fault who often paint themselves into corner by refusing to use a heap allocation or clone here and there. Once you are ok with occasional boxing, you can easily do a lot of stuff that's otherwise hard - e.g. async traits or storing/passing/moving futures like any other value.
It is quite unfair to complain on convenience of Rust async used with no heap allocations to other languages which don't offer an option to avoid boxing at all. In e.g. Java when you return a Future it is always on the heap. In Golang, you don't even have a Future concept. So you basically can't do the same thing in other languages at all.
One thing which I am afraid of, is that people who make Rust applications or libraries aren't really responsible with its advanced features (which are great!).
Like I am positive we'll be getting GAT misuse / abuse just by sheer accident because people don't understand them (they're a complex language feature), just like we have libraries going async needlessly.
There's only the ``type Future`` mess because GATs aren't exposed (they're currently compiler internal) so that's easy to fix. They could stabilize it already, but they're listening to community feedback in the respective issue (which is a fantastic read btw)
It would only be a partial fix. It will allow async interfaces - but if just "plain GATs" are exposed then those interfaces would transform into something where only Rust language experts know whats actually going on. And the majority of developers are not that, and care more about working on their actual tasks/projects and less about extremely unique features of programming languages (like GATs).
It will also not fix the "lifetimes for async functions are special" issue, due to the lazy evaluation of futures, and the "async functions might sometimes stop executing in the middle" challenge.
I can confirm this behaviour. I once made a video about the downside of Rust[1] where I also talk about the problems you encounter when using `async`. Even though it's clear that there are fundamental problems (e.g. missing support for async traits), people usually comment that I "should've used a higher-level library" to overcome this problem.
> To the point that I avoid using rust for web /networking stuff, any other option is better
How can you write networking concurrent code in Golang then? It doesn't support async traits either. Well, it doesn't support async at all, only green threads ;) And somehow people are ok with it, and say it is simple.
Hint: you can use the same style of concurrency in Rust. Don't use futures. Spawn frequently and communicate with channels.
My point was that you can program in Rust in the exactly same style as Go, not hitting any of the problems mentioned in the linked article. Just use tokio::spawn and tokio channels and you get all the stuff you have in go.
And btw, I just did that recently - I ported a proxy written in Go to Rust and actually it turned out to be simpler than the original thanks to things like join!, select!, RAII, being able to return a value from an async call easily.
Async/await is syntactic burden. In golang, everything is async so there is no need to differentiate between async/sync methods.
Goroutines = coroutines with implicit await on every function call
> And somehow people are ok with it, and say it is simple.
Threads and queues are (IMHO) fairly easy to understand and work with. If you know how you'd write an application with threads and queues, goroutines and channels work pretty much the same way.
It is not a syntactic burden, because it provides valuable information to the code reader. It is very important to me that I can quickly spot all places where execution can wait. For example, the user interface should take that into account, because noone likes programs that freeze temporarily waiting on something.
Also, await points are places where execution can jump threads - this is critical to thread safety analysis. Making them invisible in a language that doesn't offer reliable data race detection is a similar idea like making types invisible in a language with dynamic and weak typing.
And finally a model where await is implicit and automatically inserted by the compiler/runtime is less expressive (more limited) than async/await. For instance you wouldn't be able to do this if await was automatically inserted on each async thing, because they'd run sequentially and I don't want to:
let future1 = do_one_async_thing();
let future2 = do_another_async_thing();
let (res1, res2) = join!(future1, future2);
> Threads and queues are (IMHO) fairly easy to understand and work with
1. Sure, goto is easier to understand than for loops, ifs and functions. And it has its place, and I agree sometimes it can make things simpler. But I wouldn't want to work in a language where it is the only control flow construct.
2. Threads and queues are present in Rust as well, so that's not the reason Go could have an edge here.
> It is not a syntactic burden, because it provides valuable information to the code reader
"Valuable" is subjective. I personally find async/await annotations annoying and pointless, and even leading to subtle bugs if you forget to put an "await" in the right place. Code should not depend on questions like "how long will this method run".
> Making them invisible in a language that doesn't offer reliable data race detection is a similar idea like making types invisible in a language with dynamic and weak typing.
> For instance you wouldn't be able to do this if await was automatically inserted on each async thing, because they'd run sequentially and I don't want to
You can do that in Golang with channels:
ch1 := make(chan ResultType1)
ch2 := make(chan ResultType2)
go do_one_async_thing(ch1)
go do_another_async_thing(ch2)
res1 := <-ch1
res2 := <-ch2
To me, this approach is much more intuitive because communication happens explicitly through a channel, instead of implicitly through a Future that is magically returned from async functions if you don't put "await".
> Sure, goto is easier to understand than for loops, ifs and functions. And it has its place, and I agree sometimes it can make things simpler.
This is a dishonest take. Threads and queues are the most basic concurrent mechanisms, and async/await just emulates threads of execution in userspace. Golang does that too. The only difference is the lack of special syntax, and channels are used for inter-task communication instead of Futures.
> Threads and queues are present in Rust as well, so that's not the reason Go could have an edge here.
Threads in Rust are OS threads, and they are heavy. That's why the whole "async" world exists in the first place - because launching many OS threads slows down your system and takes up lot of memory.
Go takes the execution model of threads, and emulates them in userspace with implicit async runtime, so they are much faster and take much less memory.
> async/await annotations (...) leading to subtle bugs if you forget to put an "await" in the right place
In most cases if you forget to call them, the return type would be different and it would be caught by the compiler as a hard error. And if you don't use a result of something important that also leads to a warning.
However, I partially agree here with you - I can see this could lead to a potential problem when you rely on side-effects of a function that doesn't return anything. But a side effecting, async, infallible function that doesn't return anything (so it doesn't return potentially an Err) is IMHO quite a weird thing in this world. You're much better off spawning a task in such case. And spawned tasks are executed eagerly, they don't have to be awaited to run.
> To me, this approach is much more intuitive because communication happens explicitly through a channel...
So an occasional single await here and there is a syntactic burden, but having to create explicit channels to communicate the results back, instead of using function return values, is suddenly not a syntactic burden? Not mentioning a performance impact of it. You need to allocate those queues, allocate those cross-thread communication channels and then let the runtime synchronize the communication properly, and also beware of the fact that your coroutines execute in parallel (not just concurrently) and the risk of data races is higher.
> async/await just emulates threads of execution in userspace
Technically yes, but the way one program with async/await is fundamentally different than programming with threads. It is a much more structured and high level approach. The data flow is basically the same as with normal sync calls - I run some async code, I get results back as standard function return. Channels offer a lot more flexibilty, I can have a channel that delivers data from arbitrary point of thread A to arbitrary point of thread B. Very much like goto can jump from arbitrary point A to point B. See: https://vorpus.org/blog/notes-on-structured-concurrency-or-g...
> Threads in Rust are OS threads, and they are heavy.
Actually OS threads are suprprisingly fast and lightweight these days and this claim could be challenged. I've seen many threaded programs outperform programs using async and there are probably quite few usecases where green threads would offer significant performance advantage. The difference is you're scheduling them in userspace vs kernelspace, but you're still scheduling them blindly, and you still need the same synchronization primitives to coordinate the threads. And that coordination is expensive. Spawning a green thread (a task on a threadpool) running it and returning a result over an MPSC queue is still orders of magnitude slower than modifying a variable on caller's stack directly (which Rust async/await often reduces to).
But anyway, Rust has also green threads. Just use tokio::spawn + channels and those are your lightweight threads just as in Go.
> a side effecting, async, infallible function that doesn't return anything (so it doesn't return potentially an Err) is IMHO quite a weird thing in this world. You're much better off spawning a task in such case. And spawned tasks are executed eagerly, they don't have to be awaited to run.
I don't know. It just doesn't seem like "all async functions return a future by default" is useful enough feature to warrant the async/await keyword littering. It seems to me that Futures are themselves a sync concept, which is used to fuse the sync world with the async. Future methods (get, await, whatever) are all blocking.
If you discard "true" sync functions (everything that blocks an OS thread), then there's no need to fuse sync world with the async, so everything async can behave like sync, just like we're used to.
> So an occasional single await here and there is a syntactic burden
It's not the frequency of awaits that's the problem - it's the existence of await itself. The language is pushing the burden of differentiating between sync and async methods to the programmer. Of course, you can get more performance that way, but so can you by writing in assembly.
> but having to create explicit channels to communicate the results back, instead of using function return values, is suddenly not a syntactic burden?
You create channels only when you want to communicate with another (concurrent) goroutine. It seems fair to me - wanna communicate? Create a channel. Otherwise, all goroutines keep to themselves.
> Not mentioning a performance impact of it.
Channels are not queues. They are very lightweight and fast. They just work like queues.
> Which works only at runtime and detects only some races
> and also beware of the fact that your coroutines execute in parallel (not just concurrently) and the risk of data races is higher.
You can pass around pointers through channels, implicitly creating the "ownership" pattern that Rust is so famous for. Of course, there's no compiler support for it, so Rust wins this round. But in my experience working with Golang, as long as you're not trying to be too clever, data races are quite rare. Channels are really good for many communication patterns, thanks to the `select` keyword, so most of the tricky patterns that lead to data races don't even have to appear.
> Actually OS threads are suprprisingly fast and lightweight these days and this claim could be challenged.
I think this is completely false. Every new CPU vulnerability discovered slows down context switching. Default thread stack size for Windows, Linux and OS X is 1MB [0], 8MB [1] and 8MB [2], respectively (in contrast, default goroutine stack size is 8KB). Add to that the overhead of pre-emptive scheduling of threads, and you can see how they don't scale at all. After a certain fixed number of threads (depending on your hardware), your system will either run out of memory or grind to a halt, wasting all its time context switching. Meanwhile, on my laptop, Go doesn't have a problem working with hundreds of thousands of goroutines concurrently.
Here's the code if you wanna try it out for yourself:
import "fmt"
func doNothing(ch chan int) { <-ch }
func main() {
i := 0
for {
ch := make(chan int)
go doNothing(ch)
fmt.Println(i)
i++
}
}
It took ~4 million goroutine+channel pairs to fill up my 16GB of RAM. How many OS threads + queues would it take to fill up 16GB?
The optimal way to do concurrent programming is to create as many threads as there are CPU cores, and cooperatively multiplex coroutines on them. That's what the Go runtime does.
> But anyway, Rust has also green threads. Just use tokio::spawn + channels and those are your lightweight threads just as in Go.
Do those use the async/await keywords too, or are they somehow made sync-like?
I think you focus a lot only on one aspect of thread performance - when you need millions of threads. Yes, I agree, if you need millions of concurrent things running, then you quite likely don't want to use millions of threads. But many apps don't need such a high degree of concurrency, and OS threads are very performant up to a few thousand threads.
And in cases where you really want to run millions of concurrent things, and where performance and scalability are the top concern, Rust gives you a plethora of other tools.
> Do those use the async/await keywords too, or are they somehow made sync-like?
Sure they do, but now you seem to be dismissing a useful feature just because it has slightly different syntax than in your favorite language. But this is essentially the same feature.
I'm sorry if it sounded like I'm dismissing it. Async/await is very useful for massive IO in many programming languages. I've personally worked with a lot of async Python, which can be painful to write. Rust's static typing does make things much better.
However, the reason I like Go's approach so much is because it abstracts away all the mundane details about scheduling, and lets us focus on what we want. If we want 1000 threads of execution running concurrently and doing something complex, we can just `go` 1000 functions and pass the corresponding channels to them - the optimal way (MxN coroutine-thread mapping) is already invented and it's been implemented inside the language.
Rust will probably always win when it comes to extreme optimizations. Golang's strength is in simplicity and straightforwardness in writing everyday concurrent code.
You might want to look at the work the Async Foundations Working Group[1] is doing, there are indeed lots of things in flight to make the "shiny future" experience[2] better than it is today.
When async/await landed in 2018, it was an MVP. Some things have improved since, but it is incomplete. I believe the pushback you see is not "denial that it sucks", but rather an attempt to add nuance to critiques. Claiming that it is unusable or useless, like some do, is hyperbolic. There are plenty of things to fix, and people are working on some of them[3].
I also believe that landing the feature in 2018 was the right call. The parts that are pain points are features that are missing, but nothing in the currently stabilized version of the feature preclude filling those gaps. If it hadn't stabilized, what you would see is 1) many more people on nightly (which isn't ideal) and 2) a less battle tested language feature and crate ecosystem around this.
The developer experience of using async/await isn't as good as sync Rust, but it'll get better. I can point at a ton of merged PRs over the past four years as historical evidence to back that claim. And any improvement done for the underlying features that async/await uses, the result is much better experience in sync Rust too! I remember back in 2017 I spent a ton of time on improving trait bound diagnostics because until then I saw them as an "advanced feature" which didn't need immediate focus, but async/await put them front and center. The resulting work improved things for everyone, and is still ongoing to this day.
Yesterday it took me and my pair like 3 hours to figure out how to iterate over a vector of things asynchronously. Eventually we found collect'ing into a FuturesUnordered, after already implementing a repeated collect into vector of futures and then futures::join_all them.
The error messages we encountered while going through this were not the amazing one's we're used to from sync rust, these were... cryptic, to say the least.
> The error messages we encountered while going through this were not the amazing one's we're used to from sync rust, these were... cryptic, to say the least.
Please file tickets when you encounter these. A lot of the "great errors in rustc" are by their nature reactive: we need to see what people try when they hit corners that haven't gotten as much love. async/await uses multiple somewhat advanced features under the covers that have a tendency to provide either verbose or confusing errors. We're slowly improving them, but having good examples of real world cases is incredibly useful to speed up that process. This is what allows us to have extra information with recommendations.
It's been 7 months since I didn't write async code so it will be difficult for me to give u a proper answer but I'll try.
the learning curve, the cryptic lifetime compiler errors, the lack of async trait, no async Fn but async block instead, differents libraries using different runtimes, spending hours to compile a trivial code, the box pin Dyn boxfuture thingy, the compile time, issues with analyzer etc.
But most important the fact that you couldn't write code just as you would in sync rust.
Most of the features of sync rust don't compose well with async rust. At every step, async rust feels like it's a proof of concept, an half baked feature that should never had reached stable in that state.
Rushing it was a mistake to me. You shouldn't need a PhD in async await to use it properly. It should feel like writing normal rust code. Unfortunately, it's not.
Async Rust is still considered MVP, perhaps surprisingly.
> Most of the features of sync rust don't compose well with async rust.
There are two parts to that. Missing features that are required to compose the features (GATs are a good example), and bugs/incoherencies/just inconveniences.
The latter will eventually get fixed hopefully, although some of them are really hard the recent work on a-mir-formality is going to help this. The former are probably going to be solved too, but it'll be harder and perhaps require more time. As far as I can tell, nothing is impossible by the design of the async/await MVP (it was designed for that, so this is not really surprising).
FWIW, both happen in non-async code too. It's just that async code takes the type system to its extreme, and so it has more surprises.
I hope you are right (about the MVP being flexible enough to be fixed in further releases)
the problem is that the ecosystem is already built on top of multiples workarounds to overcome the problem of async rust (a.k.a the "higher level libraries"), it will be for sure a challenge to fix all the legacy code already written with the current MVP.
I don't know how they're gonna do that. that's also a reason why I don't want to have dependencies on things that may be broken in a couple of year.
I hope they will never do the same mistake. It's better to keep a half baked feature in nightly for a decade than pushing it to stable and have to deal with it forever.
Just to put your opinion in perspective: Java is unique in being huge in both academia and the industry, which bred very novel tooling around the language much more than in any other’s case (JML, many many static analyzers, etc). Java is also unique in being far the best in the tuple of performance, observability and maintainability.
Its ecosystem is one of the top 3 in size, and in many niches there is simply no alternative library for other platforms.
The language itself underwent huge changes while it remained basically fully backwards compatible. The same is true of the bytecode format.
All these make the language and platform the backbone of basically all Fortune 500 companies.
True, but I will always refuse to work with it, there's no amount of money you can pay me in to work with Java.
I get that it became the labour programming language, but I would expect people on HN to be in it for the love of computer science. I really don't think there's pleasure in writing Java.
> I mean, in my opinion the whole language Java sucks. Why anyone keep using it is beyond me.
That's a hip opinion to hold, I know, but difficult to back up with facts.
If you don't need|want C|Rust and want to write code that is extremely high performance in a language that has excellent tooling (debuggers, profilers, code analysis, etc) and top-shelf libraries and is broadly known and understood (so you can actually hire people), what are your options? Basically just one: Java.
I recently started a Rust project coming from mostly writing Haskell in my job and oh boy was I surprised. I was expecting long compile times because of the complaints I read about everywhere. But in fact compilation is very speedy. Much, much faster then I'm used to with GHC in fact.
I want to make it /very/ clear that async isn't to blame at all for the pathological build times described here. It's a bug about traits and lifetimes, both very core concepts of Rust that you deal with even if you stay away from async code.
async rust will certainly be more ergonomic once some more improvements land (hopefully later this year), but I don't feel like it deserves all the sighs it's been publicly getting these past few months. (And I /love/ to complain. I've written pieces named "Surviving Rust async interface", "Getting in and out of trouble with Rust futures", "Pin and suffering", etc.)
> Prefer dynamic dispatch to monomorphization (i.e., use fewer generics).
Unless you hit a pathological case as shown in the article, it tends to not be _that_ bad, especially if you enable `-Z share-generics=y` (unstable still, yet enabled by default for debug builds if I remember correctly).
Overall still solid advice - although "use fewer generics" sometimes turns out to be "just turn a big generic type into `Box<dyn Trait>`" (it's not _just_ boxing, that would be `Box<T>`). That's what axum[1] does with all services, and it's never had the compile times issues warp[2] had, for example.
> Don't use proc macros (i.e., don't depend on the `syn` crate, even transitively).
Good news there, I hear there's some progress on the proc-macro bridge (which improves macro expansion performance) AND "wasm proc-macros". I hope this piece of advice will be completely irrelevant in a year (but for now, it's spot-on. using pin-project-lite instead of pin-project is worth it, for example).
I know there about the bridge improvement (the amazing @nnethercote's work) but can you refer to sources on the wasm proc macros? The last thing I know about them is @dtolnay's watt.
In my experience, however, macros are usually not that problematic and `syn` is a one-time cost.
My understanding of point #2 is that LLVM may still try to devirtualize the call, which would reduce the performance impact -- is that true for Rust? I know it happens sometimes in C++.
Also for proc macros, rust-analyzer seems to struggle with them sometimes as well, so I try to avoid them (outside Serde, which is worth any price) for that reason.
rust-analyzer for a long time expands all kinds of proc-macros. The only project it does not work (although to be honest I didn't really tried) is rustc.
Async seems to be the first big "footgun" of Rust. It's widespread enough that you can't really avoid interacting with it, yet it's bad enough that it makes people resent the language.
It's really not as bad as it's made out to be. You can paint yourself into a corner with it, but a lot of that is that async is fundamentally more complicated than sync / threaded code, and there's only so much any language can do to paper that over. Rust exposes a lot of details, so it can be complicated to get to grips with how they combine with async in certain corner cases, but the happy path is quite happy even now.
A lot of the async Rust code I work with already looks like `async fn foo() -> ... { do_request().await?.blah().await }`, plus the occasional gathering of futures into a `Vec` to join on. That sort of thing, not much different from Javascript, but with a lot more control of the low-level details.
A good deal of corner cases should get better once async traits are stabilized, which will mean much less need for manually writing out Future types. But honestly, even now it's not that bad. I have a codebase that uses async to read hundreds of thousands of files[1], streaming gunzip them, pass them to another future which streaming parses records from them, and then pushes those parsed records into a `FnMut` closure for further non-async processing. It took a bit of thinking and design to get everything moving together nicely, but that corner of the codebase now is only ~200 lines of pretty straightforward code -- there's like 1 instance of `Unpin`. It's not that bad.
[1]: I know async isn't necessarily faster for reading files, but it started life doing network requests and it can still saturate a 200-core machine so I haven't felt the need to port it over to threads.
Quick aside: if you're willing to live the nightly life (unstable rustc), the `type_alias_impl_trait` feature gets you most of the way to "async trait methods". You still have to have a `Future` associated type, but in impl blocks, it just becomes `type Future = impl Future<Output = Blah>`, and then the compiler infers what the concrete (and probably unnameable, if you use async blocks) type is - no need to mess with `Pin<Box<T>>`.
The most egregious code comes when implementing one of the `AsyncRead`/`AsyncWrite` traits or similar, and that can come up a bunch in backend services, for example if you want to record metrics on how/when/where data flows, apply some limits etc. I'm curious how the ecosystem will adapt once async trait methods land for real.
FWIW I really don't like async in rust. It's improved significantly over the past couple years and it's nowhere near as bad as callback hell in Javascript but things still feel opaque. I've been toying around with a little monitoring agent (think Nagios or Sensu) to keep an eye on my defective LG fridge. So far I've managed to crash rustc twice. Trying to wrap my head around one library (that I was using incorrectly) I managed to "fork bomb" the damn thing and realize that I've little to no insight into the runtime. Try to find the current number of running tasks being managed by tokio…
The beauty of the rust async stuff is that you can move to a multi-threaded runtime as you desire with minimal effort.
> Try to find the current number of running tasks being managed by tokio…
As a heavy user of async Rust in production (at a couple places), resource leaks / lack of visibility into that has been a top issue.
In this area, tokio-console[1] is an exciting development. I have high hopes for it and adjacent tools in the future. (Instrumenting your app with tracing+opentelemetry stuff can help a lot, too).
Until those become featureful/mainstream enough, Go has the upper hand in terms of "figuring out what's going on in an async program at any given time".
> The beauty of the rust async stuff is that you can move to a multi-threaded runtime as you desire with minimal effort.
This is also a downside, having multi-thread be the default and not single-thread. It introduces some awkward / accidental trait bounds that are annoying to deal with if you want to do thread-per-core type of stuff IIRC.
I respectfully disagree; I don't think concurrency has to be that much more fundamentally complicated. It's likely that Rust's other design decisions are what made concurrency so difficult in Rust.
Pony does fearless concurrency better IMO, and Forty2 shows how we can expand on Pony to be faster and more flexible.
There are other approaches that have emerged recently too. For example, one can apply Loom's memory techniques to most memory management approaches to eliminate the coloring problem, to decouple functions from concurrency concerns.
There are also languages which separate threads' memory from each other which allows them to do non-atomic refcounting, relying on copying for any messages crossing thread boundaries (though that's often optimized away, and could be even less than Rust's clone()ing elsewhere).
One could also apply that technique to a language using generational references, if they want something without RC or tracing GC.
Sometimes I wish Rust waited just a few more years before going all-in on async/await. Alas!
Pony is garbage collected. Most of the reasons why Rust async/await are the way it is boil down to the fact that Rust is memory safe without using GC.
> Forty2 shows how we can expand on Pony to be faster and more flexible
I can't tell from a glance, but that also looks garbage collected.
> For example, one can apply Loom's memory techniques to most memory management approaches to eliminate the coloring problem
Assuming you're referring to the JVM Project Loom, that's just M:N threading. This was tried in Rust almost a decade ago. Nobody used it because the performance was not appreciably better than 1:1 threading.
> There are also languages which separate threads' memory from each other which allows them to do non-atomic refcounting
You mean like Rust? Like, that's exactly why Rust can have both Rc and Arc and still be safe.
> relying on copying for any messages crossing thread boundaries (though that's often optimized away, and could be even less than Rust's clone()ing elsewhere).
Ancient Rust did this, but it was removed because with the current immutability and borrow checking rules there is no need for copying anymore. Why would you want copying if you don't need it?
I'm also not going to just accept that clone() could be faster. I mean, I'm sure the clone codegen could be improved by better register allocation or whatever, but I don't think that's what you mean.
> One could also apply that technique to a language using generational references, if they want something without RC or tracing GC.
Why would you want to copy if you don't have to?
> Sometimes I wish Rust waited just a few more years before going all-in on async/await. Alas!
I haven't seen anything here that is better than Rust's async/await, and a lot that's either worse or doesn't fit with the rest of Rust's design.
I'd push back on "concurrency so difficult in Rust" -- because async isn't the only, or even best, way to do concurrency in Rust. I prefer using threads when I can, and Rust makes working with threads quite joyful[1]. I'd cautiously agree that it's possible async wasn't the best model to go "all in" on, though Rust is quite happily multi-paradigm so if something better comes along and has a notably different set of optimal use-cases than threads or async, I wouldn't be surprised to see Rust adopt it as well.
I'm personally sort of skeptical about "color free async" because the models for sync/blocking IO and async IO are so different -- you can paper over the syntax differences, but you're going to be in a world of hurt when the semantic differences arise[2]. I'll admit I haven't tried a color-free async implementation myself though, so it's just speculation / sour grapes :-)
> There are also languages which separate threads' memory from each other which allows them to do non-atomic refcounting, relying on copying for any messages crossing thread boundaries (though that's often optimized away, and could be even less than Rust's clone()ing elsewhere).
Curious what you mean by this -- my understanding is that Rust also does this (i.e., you can `move |x|` a value into a thread and that thread owns it now, and then the thread can hand it back in a `JoinHandle`. That sort of sharing doesn't require an Arc or Mutex, since there's only one owner at a time. Is this something different?
[1]: The other day I turned something reading in files from the filesystem sequentially into a custom threadpool passing blocks of parsed JSON over a MPSC channel that exposed the whole thing as a sequential iterator and it worked first try. I almost didn't believe it until I wrote the tests.
[2]: E.g., "I wrote this and tested it with blocking IO but this syscall isn't supported by io_uring so in async mode it goes to a threadpool and passes some huge object in a message which kills perf with a huge memcpy", or some similar jank. Just spitballing on the type of thing I would fear happening, not a specific example.
The biggest problems with "colorless async" arise with FFI. You really can't abstract over the differences between a real OS mutex and a language mutex when you're interfacing with system libraries that expect locks to actually behave like locks. Otherwise it's a recipe for deadlocks.
I really dislike threads and now I've grokked async (which, granted, took effort), I much prefer that world. I just find the design of my system is much cleaner and more robust than anything I've written in threads.
I think there's a terminology problem. To me a "footgun" is a feature which provides you with a very easy way to shoot yourself in the foot, hence the name. For example there's no way that the a[b] array index operation should default to not having bounds checks as it does in C and C++. That's a footgun. Rust does pretty well on this front, and I don't think async is especially worse.
But I can see Rust async being more of a gumption trap than many features. A gumption trap is a problem which uses up your motivation before you can work on the thing you actually wanted to do and so there's none left for the actual project.
And that's only for the aggregate, specific codepaths/code patterns got significantly faster in order to contribute to those numbers. The biggest improvement I see is 68% (bitmaps crate) back in May 25th.
AFAICT most of the talk about Rust having slow build times is a combination of kneejerk FUD from people that don't like Rust for other ideological reasons, and people that last used Rust when it was much earlier in development. (And also people that have some massive project with hundreds of dependencies that would be slow to build in any language)
I don't get the complaints either, especially because so many people seem to treat it as an actually serious issue and not just the meme it is. Yes, many other compilers are faster and produce smaller binaries. But it's not that bad and I get incredible value out of all the checks done by rustc. Incremental builds are reasonably quick and produce excellent error messages.
When it comes to efficiency optimizations I see much lower hanging fruits in the web space, some TypeScript projects I'm working on take longer to fully start the dev setup than a fresh optimized Rust build. I hope Bun will make the difference it promises.
It gets pretty bad as your codebase increases. I work on a pretty large Rust project that uses a lot of async, and uses a LOT of static dispatch (mainly because we use a web-server framework called warp).
Usually, even after a simple change a simple `cargo check` can take a minute or two on a beefy PC. That said, over time you get numbed to it :D.
I can't say much about async because I never really use it. To me it feels like a slightly misleading abstraction/syntax compared to what actually happens, most bugs I introduced in JS backends happened because async/await made the code look more synchronous than it is. Maybe that's just me.
I think a bunch of the "rustc compiles slow" talk comes from the misunderstanding of what rustc does.
For example, to do the same things as `rustc` but using C++, you need to run external runtime modifying program to check your program for memory errors .. so that extends your C++ "compile time" by fair amount to even begin to match what rustc does.
The question would be for me, can we run those slow-rustc cases faster by skipping the part corresponding to "external checker program"? Or is that part required for each and every build, even after tiny changes or no changes at all?
Rust has a fairly powerful incremental compilation engine, so it tries to avoid re-computing checks if the relevant code hasn't changed. In practice it still recomputes a lot of stuff though.
It's not that relevant though, because the longest part of any Rust build is the dependencies, and those are never recompiled. Most projects compile relatively fast once you've built them once.
Well, you are asking about disabling borrow checker here, one of the main selling points of Rust and rustc here so I think that's out of the question.
When you do full comparison of what rustc does, and compare it to what you need to do in C++ project to match it, the compile times aren't actually slow since rustc does more with actual guarantees.
But that was my point, I don't necessarily want to do more. In general I want results faster, and am not willing to trade build speed for more safety checks unconditionally.
Having some additional features and checkers is nice, and can even be a life saver, but having to pay a high cost each time I hit compile is not acceptable.
To expand on my other answer, part of the problem is that Rust's compile time complexity is in large part spent on knowing what code to execute, not just whether the code is valid.
So the compiler will spend some time inferring the types of different variables and what traits those types implement, which is necessary to know which trait methods get called, etc.
Dynamically-typed languages get to skip all this and say "my type is either a number or an object, because everything in the universe is either a number or an object", and methods are selected through dynamic dispatch (usually by storing a ton of function pointers everywhere).
Rust can't just do dynamic typing and skip the compile-time logic, though you can get close by using lots of dyn traits.
Every time I read one of these posts digging into logs and flsmegraphs I think "man, there really should be a simpler way to understand what a compiler is doing when you feed it code".
Author here: I honestly expected it to be much worse. The tracing integration in particular is fantastic - being able to just slap a `#[instrument(...)]` attribute on any function and then be able to filter which spans you want to see is kind of a superpower.
That said I think much could be improved, still. I only breezed through the perf/nperf stuff because I've used them before, and through the self-profiling stuff because rustc devs helped me with it.
I would've killed for a REPL while I was working on this (or a server architecture so I could write my _own_ rustc queries), and step through them, etc. I like Kate's approach to this[1] - I'd just like something a little more... interactive.
What do _you_ think it should look like? I feel like a ton of good ideas come from folks who "simply didn't know it was impossible" (more accurately: haven't been trained to accept to work with subpar tools). I'm excited to try out Pernosco[2] for example, it seems like a much-needed rethink of debuggers.
Thinking "things should be better" was actually part of what led me down the rabbit hole of working on the Druid crate, and then starting my own crates (Panoramix and an unreleased druid fork).
Like, you know how, when you hover your mouse over a DOM node in the DOM inspector, Chrome/Firefox will automatically highlight the rendered node on your page? I'd want that for code.
Like, I'd want to be able to hover on that `Binder(ProjectionPredicate(...)` dump, and have the IDE highlight the span in the input code of the `where clause` this matches.
(Or like, not in the input code, but in a buffer showing one of the intermediate representations.)
Other things I'd want:
- Being able to alt-click on the `Binder` log and open a page showing Binger's documentation, instead of having to ctrl+shift+f it in the code.
- Being able to select a value I don't like, and jump to the call stack that generated that value, inspect variables in that call stack, etc.
- Being able to hot-reload and execute spans with different parameters. Eg I'd want to be able to rewrite a function, and have it be recompiled on the fly, and called with the same parameters as before, and see the changes and new logs propagate. Of course this only works if the function is pure and part of a pass system; the idea being that the IDE would cache the previous passes.
I really liked Pernosco when I first saw it! I haven't had the occasion to use it yet, but I really like the principles behind it.
I've also heard good things about Jaeger, and the UI looks good in your screenshots. I might try to use it if I ever give working on rustc another try.
Having written a huge amount of heavily templated C++ and a moderate amount of heavily generic’d Rust, I prefer Rust any day. No more ‘typename’ for me thank you.
I also did my share of optimizing C and C++ builds, however while the worst case can turn into hours, in most places it isn't as bad, because the communities do embrace binary libraries instead of compiling all third party dependencies (or other in-house teams) from scratch.
Except building the same crates multiple times across the depedency graph.
Additionally, from my point of view, not caring about initial build times is a crappy development experience, versus what is offered by apt/yum/NuGet/vcpkg/conan.
All of the packages I'm using for my most recent project (https://github.com/zig-for/snfm/blob/main/vcpkg.json) required building on machine. Takes about an hour and doesn't run in parallel (now there's a good PR for someone...). Vcpkg packages _can_ be distributed in binary form, it just doesn't happen.
warp is quite a nice framework but it has a really bad tendency to blow up compiletimes in my experience. I think for non trivial projects in the future I will prefer something else because it can be a real sore spot.
Warp is really cool from a fun "look what we can do with the type system" way, and I enjoyed it for a pet project. However, when we used it at work, it quickly became incredibly annoying in a larger team. My main gripe with it is it's unusual (which o previously thought was cool) way of building the routing tree, which impedes visibility in code. I.e. I can't just grep for a route; I need to walk the tree to find a handler. There's also a few other things it doesn't handle that well, like streams. However, you're right that the compile times are an issue too.
I much enjoy Axum now though, and whose API slightly reminds me of Bevy's ECS; in its practical use of the type system to achieve ergonomics.
> The crux of the problem is "gee grandma, what big types you have there", because... essentially, tower is a couple of important traits.
> And the basic idea is "oh shit we don't have async trait methods" (hence the Future associated type) but also "backpressure is love, backpressure is life", by way of the poll_ready method.
I really don't like this guys style of writing. It seems like the bones of the article are interesting, but then they slap like three layers of meme talk, irony and sarcasm, and it just ruins the read for me. It reminds me of TARS humor setting. It doesn't need to be zero, but its currently at like 98, can we turn it down some?
It's borderline, but this probably falls under the 'tangential annoyances' that the site guidelines ask commenters not to complain about:
"Please don't complain about tangential annoyances—things like article or website formats, name collisions, or back-button breakage. They're too common to be interesting."
The point isn't that such things aren't annoying, it's that commenting about them leads to shallow off-topic tangents in the thread, which only piles one annoyance on top of another.
Come on now. There's two chasms between "an academic paper", my blog, and "a blog with minion/the office reaction GIFs".
The amount of humor is finely-tuned so that it's not /too/ distracting from the actual technical topic at hand (of which there's always one, I'm not a sit-down comic), but it also filters out folks who'd rather focus on the form than the content / take themselves too seriously.
Isn't the TikTok equivalent of a blog post just a Tweet? I mean, back when the first came out there was a whole notion of "microblogging" that they were tapping into.
This post has paragraphs, sections, coherent layout. It's certainly not a TikTok analogue.
Rough. It seems like there's a lot of poor engineering in the compiler :( I almost wonder if the new gcc backend will be better but then I remember clang is significantly faster than gcc
It's something I've only encountered with rust community, in Java when a feature sucks, we just say it sucks. Even the architect that approved it will say it sucks.