There’s a lot to criticize about Rust for sure, but I feel like some of the points here aren’t necessarily in good faith.
> Explicit destruction (under the hood) of every object. It's slow.
Care to actually support this with data? C++ is quite similar in this respect (Rust has a cleaner implementation of destruction) and generally outperforms any GC language because stack deallocation >> RC >> GC in terms of speed. There’s also a lot of good properties of deterministic destruction vs non deterministic but generally rust’s approach offers best overall latency and throughput in real world code. And of course trivial objects don’t get any destruction due to compiler optimizations (trivially live on the stack). And zig isn’t immune from this afaik - it’s a trade off you have to pick and zig should be closer since it’s also targeting systems programmers.
> - Many memory-safe programs won't type-check (impossible to avoid in any perfectly memory-safe language, but particularly annoying in Rust because even simple and common data structures get caught in the crossfire).
Actually most memory safe languages don’t have issues expressing data structures (eg Java). And rust has consistently improved its type checker to make more things ergonomic. And finally if you define rust as language + stdlib which is the most common experience those typical data structures are just there for you to use. So more of a theoretical problem than a real one for data structures specifically.
> Even in safe Rust, you still have a form of subtle data race possible, especially on ARM.
I agree that for the language it’s weird that this is considered “safe”. Of course it’s not any less safe than any other language that exposes atomics so it’s weird to imply this as something uniquely negative to Rust.
> Even in safe Rust, races like deadlocks and livelocks are possible.
I’m not aware of any language that can defend against this as it’s classically an undecidable problem if I recall correctly. You can layer in your own deadlock and livelock detectors that are however relevant to you but this is not uniquely positive or negative to rust so again weird to raise as a criticism of Rust.
> The constraints Rust places on your code tend to push people toward making leaky data structures. In every long-running Rust process I've seen of any complexity (small, biased sample -- take with a grain of salt), there were memory leaks which weren't trivial to root out.
I think you’re right to caution to take this with salt. That hasn’t been my experience but of course we might be looking at different classes of code so it might be more idiomatic somewhere.
> In the remaining use-cases, the right thing to do is almost always to put everything into a container object with its own lifetime
You can of course do that with Rust boxing everything and/or putting it into a container which reduces 99% of all lifetime complexity. There are performance costs of doing that of course so that may be why it’s no considered particularly idiomatic.
My overall point is that it feels like you’ve excessively dramatized the costs associated with writing in Rust to justify the argument that memory safety comes with excessive cost. The strongest argument is that certain “natural” ways to write things run into the borrow checker as implemented today (the next gen I believe is coming next year which will accept even more valid code you would encounter in practice although certain data structures of course remain requiring unsafe like doubly linked lists which should be used rarely if ever)
The issue with destructors being slow is actually a well-known problem with C++, particularly on process shutdown when huge object graphs often end up being recursively destructed for no practical benefit whatsoever (since all they do is release OS resources that are going to be released by the OS itself when process exits).
Comparing stack deallocation vs GC is kinda weird because it's not an either-or - many GC languages will happily let you stack-allocate just the same (e.g. `struct` in C#) for the same performance profile. It's when you can't stack-allocate that the difference between deterministic memory management vs tracing GC become important.
Also, refcounting is not superior to GC in terms of speed, generally speaking, because GC (esp. compacting ones) can release multiple objects at once in the same manner as cleaning up the stack, with a single pointer op. Refcounting in a multithreaded environment additionally requires atomics, which aren't free, either. What refcounting gives you is predictability of deallocations, not raw speed. Which, to be fair, is often more important for perception of speed, as in e.g. UI where a sudden GC in the middle of a redraw would produce visible stutter.
> Also, refcounting is not superior to GC in terms of speed, generally speaking, because GC (esp. compacting ones) can release multiple objects at once in the same manner as cleaning up the stack, with a single pointer op. Refcounting in a multithreaded environment additionally requires atomics, which aren't free, either. What refcounting gives you is predictability of deallocations, not raw speed. Which, to be fair, is often more important for perception of speed, as in e.g. UI where a sudden GC in the middle of a redraw would produce visible stutter.
In practice, tail latencies are much harder to control in GC vs RC implementations which is what I was trying to communicate. This doesn’t matter just for UI applications but can also directly implicate how much load your server can service. Ref counting in a multithreaded environment can use atomics although biased ref counting is considered the state of the art to minimize that cost (ie RC on the owning thread, arc on shared threads).
As for releasing multiple objects at once, in practice I’ve yet to see that bear out in practice as a real advantage. The cost of walking the graph tends to dominate vs RC where you precisely release when unreferenced. And that’s assuming you even use RC - often times you at most RC at the outermost layer and everything internally is direct ownership. And if you really do need that, use an arena allocator which gives you that property without the need for a GC collection pause. There’s a reason there’s no systems language that uses GC.
> The issue with destructors being slow is actually a well-known problem with C++, particularly on process shutdown when huge object graphs often end up being recursively destructed for no practical benefit whatsoever (since all they do is release OS resources that are going to be released by the OS itself when process exits).
If you want fast shutdown just call _Exit(0) to bypass destructors of static, thread local, automatic storage duration. GC languages have a much worse problem of making it really easy to leak resources during the execution of a long running program. I’ll take that over a slow shutdown anytime, especially since in practice, unless you’ve written really bad code, that “slow shutdown” remains negligible.
> There’s a reason there’s no systems language that uses GC.
There are a few system languages that uses GC, like Nim and D. Of course with the option to do manual memory management where necessary, and allocating things on the stack whenever possible. Nim also gives option for several diferent types of GCs and memory allocators, where each one can be more performant for different tasks. Maximum GC pause can also be configurable, at the cost of temporarily using more memory than you should until the GC manages to catch up.
Of course, you can always manually craft arenas and such to be faster and avoid fragmentation, at the cost of much more effort.
Nim and D both offer multiple GC strategies within the language. Just as with C and Rust, while they can be used for systems programming, they can also be used for other things. If you’re doing systems level programming with them you’re probably not choosing any tracing GC option.
Nim and D are also bad examples as I’m not aware of any meaningful systems level programs that have been written in them - they have continuously failed to find a way to become mainstream (Nim is mildly more successful in that it’s managed to break into the 50-100 range of most popular languages but that’s already well into the tail of languages to the point where you can’t even tell the difference between 50 and 100)
I used to use Rust for work, and I use Zig in my new job. They're both fine. It was a good-faith smattering of examples, and it's pretty easy to keep pulling such examples out of a hat.
You seem to not like any of them much, so I'll just briefly address a few of your points:
> Of course it’s not any less safe than any other language that exposes atomics so it’s weird to imply this as something uniquely negative to Rust
That wasn't the implication. Off-the-cuff, when you ask your average rustacean what they think "no data races in safe Rust" means, do you honestly think they will tend to write code treating atomics with an appropriate level of respect as they would in another language?
> Actually most memory safe languages don’t have issues expressing data structures (eg Java)
That was sloppy writing on my part. I left the implicit "without runtime overhead" in my head instead of writing it down.
> Memory leaks
This first one isn't a leak per se, but it's about the same from an end-user perspective [0]. Here's a fun example of that language complexity I was talking about (async not being very composable with everything else) as an example of a true leak [1]. Actix was still only probably/mostly leak-free starting from v3 [2].
Rust makes it easy to avoid UAF errors, but the coding patterns it promotes to make that happen, especially when trying to write fast, predictably performant data structures, strongly encourage the formation of leaks -- can't have a UAF if you never free.
> Off-the-cuff, when you ask your average rustacean what they think "no data races in safe Rust" means, do you honestly think they will tend to write code treating atomics with an appropriate level of respect as they would in another language?
I agree, from what you would expect from Rust, atomics are a weird safety hole. But that’s just because the bar for Rust is higher but if we’re comparing across languages we must use a consistent bar.
> This first one isn't a leak per se, but it's about the same from an end-user perspective [0]
This kind of stuff pops up in every language (eg c++ vector and needing to call shrink_to_fit). Reusing allocations isn’t a unique problem to Rust and again, if you’re using the same bar across languages, they all have similar issues. I’m sure zig does too if you go looking for similar kinds of footguns, especially as more code starts using it.
> Rust makes it easy to avoid UAF errors, but the coding patterns it promotes to make that happen, especially when trying to write fast, predictably performant data structures, strongly encourage the formation of leaks -- can't have a UAF if you never free.
There’s so many cutting edge performant concurrent data structures available on crates.io that let you do cool stuff with respect to avoiding UAF and not leaking memory when you really need it. And other times you don’t need to worry about concurrency and then the leak and UAF concerns go away too. And again, I feel like a higher bar is being used for Rust and it doesn’t feel like Zig or other languages really offer more ergonomic solutions
> Explicit destruction (under the hood) of every object. It's slow.
Care to actually support this with data? C++ is quite similar in this respect (Rust has a cleaner implementation of destruction) and generally outperforms any GC language because stack deallocation >> RC >> GC in terms of speed. There’s also a lot of good properties of deterministic destruction vs non deterministic but generally rust’s approach offers best overall latency and throughput in real world code. And of course trivial objects don’t get any destruction due to compiler optimizations (trivially live on the stack). And zig isn’t immune from this afaik - it’s a trade off you have to pick and zig should be closer since it’s also targeting systems programmers.
> - Many memory-safe programs won't type-check (impossible to avoid in any perfectly memory-safe language, but particularly annoying in Rust because even simple and common data structures get caught in the crossfire).
Actually most memory safe languages don’t have issues expressing data structures (eg Java). And rust has consistently improved its type checker to make more things ergonomic. And finally if you define rust as language + stdlib which is the most common experience those typical data structures are just there for you to use. So more of a theoretical problem than a real one for data structures specifically.
> Even in safe Rust, you still have a form of subtle data race possible, especially on ARM.
I agree that for the language it’s weird that this is considered “safe”. Of course it’s not any less safe than any other language that exposes atomics so it’s weird to imply this as something uniquely negative to Rust.
> Even in safe Rust, races like deadlocks and livelocks are possible.
I’m not aware of any language that can defend against this as it’s classically an undecidable problem if I recall correctly. You can layer in your own deadlock and livelock detectors that are however relevant to you but this is not uniquely positive or negative to rust so again weird to raise as a criticism of Rust.
> The constraints Rust places on your code tend to push people toward making leaky data structures. In every long-running Rust process I've seen of any complexity (small, biased sample -- take with a grain of salt), there were memory leaks which weren't trivial to root out.
I think you’re right to caution to take this with salt. That hasn’t been my experience but of course we might be looking at different classes of code so it might be more idiomatic somewhere.
> In the remaining use-cases, the right thing to do is almost always to put everything into a container object with its own lifetime
You can of course do that with Rust boxing everything and/or putting it into a container which reduces 99% of all lifetime complexity. There are performance costs of doing that of course so that may be why it’s no considered particularly idiomatic.
My overall point is that it feels like you’ve excessively dramatized the costs associated with writing in Rust to justify the argument that memory safety comes with excessive cost. The strongest argument is that certain “natural” ways to write things run into the borrow checker as implemented today (the next gen I believe is coming next year which will accept even more valid code you would encounter in practice although certain data structures of course remain requiring unsafe like doubly linked lists which should be used rarely if ever)