7 ms·
1. Don't use the `async` ecosystem. 2. Prefer dynamic dispatch to monomorphization (i.e., use fewer generics). 3. Don't use proc macros (i.e., don't depend on
by indiv0 4y ago
1. Don't use the `async` ecosystem.
2. Prefer dynamic dispatch to monomorphization (i.e., use fewer generics).
3. Don't use proc macros (i.e., don't depend on the `syn` crate, even transitively).
Easy to say; hard to put into practice. But that's all there is to it.
- fasterthanlime 4y ago> Don't use the `async` ecosystem. 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). [1] https://lib.rs/crates/axum https://lib.rs/crates/axum [2] https://lib.rs/crates/warp https://lib.rs/crates/warp
- vgel 4y agoMy 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.
- afdbcreid 4y agorust-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.
- afdbcreid 4y agoI 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.
- spekcular 4y agoWhat forthcoming improvements to async are you referring to?
- aaaaaaaaaaab 4y agoAsync 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.
- vgel 4y agoIt'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.
- fasterthanlime 4y agoQuick 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.
- staticassertion 4y agoOr don't, because all of those things are great. Compile times aren't that bad unless you're doing a clean uncached build, which is very unlikely.
- tayo42 4y agoHow are people writing event loop based webservices with rust then if they're not using async?
- steveklabnik 4y ago(I don't think async is bad.) Some folks just use threads, no event loop. Event loops are also there without async, you could just write against mio or whatever else you choose directly.