Hacker News new | ask | show | jobs
by mplanchard 1299 days ago
I came to rust mostly from higher level languages, and I didn’t have as much of a hard time with lifetimes as I did with dynamic dispatch via trait objects. Lifetimes can certainly get hairy, but it’s easy to get around them when you’re learning by cloning a value, wrapping it in an Arc, or whatever. I have had multiple times where I wanted to use dynamic dispatch and discovered after a few days of work that my idea wouldn’t work for one reason or another. Mostly this came down to some limitations on what trait objects can and can’t do, but it has mostly ceased to be a problem now that I understand those limitations better.
3 comments

You didn't have a hard time with the borrow checker because you used the Rust GC? Was that the argument?

Actually I left Rust and moved to Go although I love Rust much more because every struct ended up sprinkled with Arc.

No, there’s no argument. The statement was that lifetimes aren’t that hard, and that when you do get stuck there are escape hatches. The escape hatches are especially useful when you’re still learning.

FWIW I write Rust professionally now, and in our entire codebase we have maybe four or five data structures containing Arcs, specifically where objects are spawned as Futures on other threads.

That would be a very interesting metric, could you grep|wc them and the locs?
Sure!

Tokei says that we've got ~76k lines of Rust.

I used ripgrep to find occurrences of `Arc::` (to exclude type signatures and only find constructors) and then just gave it a quick manual look to exclude tests and benchmarks.

This leaves me with:

- one data structure that contains general application state and contextual information, which is created at application startup and shared across all tasks/threads

- one data structure for accumulating results that is shared across threads. This is an Arc<Mutex<u64, SomeEnum>>. The less common of the two enum variants is an Arc<BTreeMap>.

- one data structure used to pass a database connection in to a context where it'll be used to spawn async tasks

All told, we've only got four places in production paths where we construct a new Arc. One of those four is only called once in the life cycle of the application, while the others are called for any given invocation.

You're comment made my day, thanks for your work!
Hey no problem! I was glad my guess wasn’t too far off, and it was fun to go digging to check.
Cloning values and using Arc to wrap structs carry a huge performance costs. It's pretty easy to write correct Rust. It's a lot trickier to write fast Rust.

That's where the learning curve started to get very steep for me.

Is the performance difference meaningful? I write Python, so I suspect even poorly ported Rust will outperform artisanally crafted Python.
Yes. Most of the production use cases for Rust are aimed at C, C++, and Go users. In my past experience, even poorly-written Go tends to outperform poorly-written Rust for many tasks, not to mention C or C++.

You're not exactly the target market if you're writing python.

But that is kind of my point. There are plenty of reasons to use Rust that have nothing to do with performance. My brief toe-dip into the Rust world loves the static types, good dependency management, and single executable deployment. That it is faster than my standard language is just the cherry on top.
Yeah, Rust is a really fun language to write in my opinion. If your use-cases are tolerant of non-hyper-optimized Rust, there’s no reason to make it too hard on yourself while you’re learning. As you use it, I think you’ll naturally gravitate towards writing more optimized code, because the language guides you in that direction.
The unfortunate reality for many Rust fans is that a lot of people would prefer to use a less complicated language when performance doesn't matter.

That Rust program you spent a day golfing to make beautiful use of iterators? Your friend wrote it in go in 15 minutes.

They also get static types, good dependency management, and singe executable deployment. Cython + mypy basically gives you that with Python.

I don’t know why this would be an unfortunate reality. People should use whatever language they want.

Rust gives you a lot of ways to express yourself, but now that I am quite familiar with it, I can write Rust just as quickly as I can write Python or Go.

You can spend all day code golfing in any language.

This conversation was about learning Rust, not about writing maximally optimized Rust. Clones and Arcs can make learning a lot easier, since they let you get stuff done without needing to figure out every obscure lifetime error.

For production contexts, we take more care to optimize for performance.

You and I have very different definitions of "maximally optimized." I think of eliminating all possible wasted CPU cycles when I hear those words. In comparison, being able to remove spurious copies and atomic accesses is table stakes for claiming that you know a systems programming language. Most use cases of C and C++ today are in that state.
Well, clones can sometimes make it faster (e.g. when you use multiple threads that can work independently, synchronization will have a much higher overhead than a literally insanely fast, predictable memory-to-memory copy). And then the compiler may very well be able to elide the copy, it only needs it for semantics.
You’re right, I was exaggerating for effect. But the point is that we’re talking about learning the language. You don’t have to get it perfect on your first try or do everything the best way when you’re just getting started.
Yeah, but Python is the slowest language ever. I think that a decent language like D or Common Lisp would outperform poorly written Rust, and they're easier to handle.
It really depends on what you're building whether that perf cost is problematic or not, and usually it isn't. If 90% of your code is clean Rust and the last 10% outside of the critical path is a straightforward clone or Arc, then I see no reason not to go that way.
> and using Arc to wrap structs carry a huge performance costs

Does it? It's like using shared_pointer, which some C++ codebases do everywhere for memory safety.

And some languages like Swift even do it by default.

Huge is maybe a strong term. The cost of a clone is hugely dependent on the size of the data being cloned. Avoiding clones of large data structures is important, but even that is unlikely to be a bottleneck outside of a hot path.

Arcs can be expensive, but once you’ve got the sense for lifetimes, they aren’t that hard to avoid.

> Mostly this came down to some limitations on what trait objects can and can’t do, but it has mostly ceased to be a problem now that I understand those limitations better.

Care to recount what some of those misunderstandings were? I’m casually interested in Rust but only really observe from afar, since most of my day job is Swift. Don’t get me wrong, Swift has some odd limitations around protocols (closest equivalent to traits) that may be similar, but I’m curious to see what some common pitfalls may be with Rust traits.

I’m sorry I don’t have specific examples because it’s been a while, but IIRC generally the issues came from trying to mix compile-time dynamism with runtime dynamism. Things like depending on methods that made traits non-object-safe and so on.

I think it can be quite confusing initially how traits are both the unit of compile-time generics AND can be used for dynamic dispatch at runtime, given that there are separate rules about what can be used in which context.