7 ms·
> Most of the pain here come from the unholy trifactor: combining async, lifetimes and dynamic dispatch with trait object closures; which is indeed very awkward
by ekidd 4y ago
> Most of the pain here come from the unholy trifactor: combining async, lifetimes and dynamic dispatch with trait object closures; which is indeed very awkward in practice.
Even in regular Rust, trying to get too clever with lifetimes can cause serious pain. The usual culprit is complex code that tries to never allocate memory. "Oh, well this closure borrows this parameter from the parent function, and then stores a reference to it in this stack-based structure inside the closure, and then we pass everything by reference to this higher order function..." Just say no.
If you add async to the mix, then you need to keep your ownership simple. If you have a long-running async function, then pass parameters by value! If you have a polymorphic async function, then return your result in a Box. (This also breaks up the generated state machine used to implement async functions, and can reduce binary size.)
So much of this pain is caused by premature optimization.
In C++, if you get to clever, you eventually make a mistake and segfault. In Rust, if you get too clever, it eventually becomes impossible to satisfy the borrow checker. Most of the time, the solution is to be less clever.
Async Rust was a fascinating experiment, but in practice, I think it turned out to be a tool that's best used conservatively. I've written some very stable and high-performance production code using async Rust. But I keep it simple when I can.
- crabbygrabby 4y agoThis is good advice. Write simple code first. Another generic thing I've come across is, "if the borrow checker is screaming at me when I hit a patch of code, I probably have a design flaw not a programming problem". I've seen a lot of new people also prematurely over generalizing code. I know it sounds terrible to say, but if you probably aren't going to reuse it, you really don't have to prepare your code base just in case you might later. Decide that later and move along...
- FpUser 4y ago>"if the borrow checker is screaming at me when I hit a patch of code, I probably have a design flaw not a programming problem". This reminds me: "you are holding it wrong"
- estebank 4y agoIn industrial machines, there's such a thing as holding it wrong. In some cases, that means getting maimed (that old-school table saw doesn't care whether it chews on wood or flesh) or precluded through a clunkier mode of operation (you need to press these two buttons separated at roughly arm length to make sure that they can't be actioned while you have an arm in the way of the heavy arm-eating chunk of metal). The former is C and unsafe Rust, the later is safe Rust and GC languages. You're trying to draw a comparison with a consumer product with a design flaw.
- FpUser 4y ago>"You're trying to draw a comparison with a consumer product with a design flaw." Not at all. I just do not like to dance around bonfire with tam-tam and being told that this is the one and only way
- kaba0 4y agoIt's not the only way, but if you do the tradeoff of going with manual memory management, then you should accept what the tradeoff entails. Essential complexity is non-reducible -- you either manage it automatically through a GC or choose a model that while let you ignore it quite often, will make you think about it in the end.
- josephg 4y ago> If you have a long-running async function, then pass parameters by value! If you have a polymorphic async function, then return your result in a Box. I've taken to making heavy use of the smallvec and smartstring crates for this. Most lists and strings are small in practice. Using smallvec / smartstring lets you keep most clone() calls allocation-free. This in turn lets you use owned objects, which are easier to reason about - for you and the borrow checker. And you keep a lot of the performance of just passing around references. I tried to use async rust a couple of years ago, and fell on my face in the process. Most of my rust at the moment is designed to compile to wasm - and then I'm leaning on nodejs for networking and IO. Writing async networked code is oh so much easier to reason about in javascript. When GAT, TAIT and some other language features to fix async land I'll muster up the courage to make another attempt. But rust's progress at fixing these problems feels painfully slow. https://crates.io/crates/smallvec https://crates.io/crates/smallvec / https://crates.io/crates/smartstring https://crates.io/crates/smartstring
- rapsey 4y agoHowever the constant checking if something is on the stack or heap is a big performance problem for smallvec. It results in terrible cpu branch prediction.
- kaba0 4y agoI don't see how is it bad for branch prediction. Like, if you use the same smallvec object in a loop and do not insert any elements to it (so that it doesn't allocate on the heap) the branch will always turn in the same direction making it easy and cheap to predict. And I would think that in most use-cases your smallvec object will either remain in one state, not changing in-between.
- nmca 4y ago+1, I would love to see a trace of smallvec killing performance due to branch prediction.
- bsder 4y ago> If you have a polymorphic async function, then return your result in a Box. Agreed. Copy, Clone, Box, Arc, RwLock, etc. are your friends. Don't be afraid to use them. You don't need as much performance as you think you do. Passing things around on the heap is fine for most applications. The Rust compiler is often smarter than you think. And, when the Rust compiler is dumb or your code is getting stuck, you have code that you can understand and find the hotspot of. Now you can be clever.
- mwcampbell 4y ago> So much of this pain is caused by premature optimization. Part of the problem is that Rust itself has such lofty aspirations to do it all (particularly "abstraction without overhead"), and is largely successful at that. So if I compromise on efficiency, it feels like my code is unworthy of the language it's written in. Also, it's easy to feel that the slightest inefficiency puts us on a slippery slope to the bloat and slowness of something like Electron. I'd like to fight back against that bloat, the upgrade treadmill, and the rapid obsolescence that serves hardware makers but hurts poor people. On the one hand, there was good software in the 80s and 90s that made liberal use of heap-allocated, reference-counted objects with dynamic dispatch. On the other hand, there were slow, bloated desktop applications before the rise of Java, C#, Electron, etc. So I don't know where the right balance is at.
- xyzzy_plugh 4y agoYou hit the nail on the head, at least for me. I've found Go to be the right sweet spot between efficiency and productivity. I can't get anything done in Rust. My worst enemy is myself.
- galangalalgol 4y agoI like the thick stdlib in go, but it feels a lot slower even than java. The thin stdlib and giant dependency trees is my primary complaint with rust. I like most of the syntax, but async is not my threading style, I mostly use fine grained threads like rayon provides. I'm currently more excited about julia though. Same dependency hell, but super fast and flexible, and the code looks good.
- teaearlgraycold 4y agoStep one is just clone() everywhere all the time. Then you should cut back AFTER analyzing where all of your memory is being lost to.
- chombier 4y agoCan one really escape clone hell afterwards, though?
- wongarsu 4y agoI generally go by the principle to borrow if I can without effort, otherwise I think a second of the code is in the critical path or the object is megabytes in size. If neither is the case, I clone without shame. The remaining 0.1% gets my attention (Arc, lifetime annotations, refactoring, or whatever is appropriate). The important part is perspective. Nobody cares if you clone your command line arguments ten times during startup. As Dijkstra said, optimize the 2% of your program that matter, keep the rest simple
- kaba0 4y agoExactly. Even very experienced people will be easily misled to believe something is performance-sensitive -- computers are ridiculously fast. Like honestly, if it is not a tight loop chances are it seriously doesn't matter what you do. Remember, we write plenty of shell scripts that literally spawn a new process for almost every line, and yet they feel instantaneous.
- anonymoushn 4y agonever allocating memory can be and should be simple.
- bpye 4y agoWhat language actually makes it simple?
- anonymoushn 4y agoIn Zig, we are writing a moderately complex application that calls mmap many times in the first few seconds and never thereafter. We can use every part of the stdlib and most libraries because we can pass in allocators backed by the memory we reserved at startup.
- jedisct1 4y agoZig, definitely.
- ncmncm 4y agoThis is very commonly done in C++. My programs do a flurry of mmaps in the first second, then run for, sometimes, years without allocating again.
- brabel 4y ago> So much of this pain is caused by premature optimization. Rust is normally used only when high performance is of uttermost importance, so it will always attract people who want to optmise everything. > Most of the time, the solution is to be less clever I've done it myself and saw performance degrade, as was expected. That's fine, but by that point you might as well just use another language that has none of the hard problems and end up having very similar performance, which is what I did.
- Animats 4y ago> Rust is normally used only when high performance is of uttermost importance, so it will always attract people who want to optimise everything. Which is not the way to do things. Profile, then optimize. I'm writing a metaverse client that's heavily multithreaded and can keep a GPU, a dozen CPUs, and a network connection busy. Only some parts have to go fast. The critical parts are: * The render loop, which is in its own higher-priority thread. * Blocking the render loop with locks set during GPU content updating, which is supposed to be done in parallel with rendering. * JPEG 2000 decoding, which eats up too much time and for which 10x faster decoders are available. * Strategies for deciding which content to load first. * Strategies for deciding what doesn't have to be drawn. Those really matter. The rest is either minor, infrequent, or not on the critical path. I use Tracy to let me watch and zoom in on where the time goes in each rendered frame. Unless Tracy says performance is a problem, it doesn't need to be optimized.
- SleepyMyroslav 4y agoComing from gaming industry i think you might want to measure how far you can go with a single threaded rendering. There is limit of content and code that can be a brick wall later. Here is an example from SIGGRAPH 2021 where Activision presents how multithreaded rendering looks like: https://youtu.be/9ublsQNbv6I https://youtu.be/9ublsQNbv6I ps I don't work with Activision its just a public example that illustrates industry practice.
- Animats 4y ago
- zepearl 4y agoThis fully hits what I experienced. I'm using Rust since ~2 months. At the beginning I was trying to write my usual C/C++ code and I got completely entangled with lifetimes relationships and the code ended up becoming a mess. Nowadays as soon as the compiler starts mentioning lifetimes I see that as a warning => I then take a step back and change approach/design => no problems. I use a little bit of async (I create dedicated variables that are moved to the async functions) and classic multithreading (I use "mpsc" to exchange data between the main & subthreads) and so far I never had a single segfault nor any kind of weird behaviour, which is incredible if compared to some other languages at least for my programming skills :)