Ownership, performance, and when cloning is actually the right choice?

Hi everyone,

I’m still getting comfortable with Rust’s ownership and borrowing model, and I keep running into some confusion around performance vs code simplicity.

In a lot of situations, the most straightforward way to satisfy the borrow checker is to just clone() data like String or Vec<T>. The code becomes clearer and easier to reason about, but I’m never fully sure if I’m introducing unnecessary allocations or if this is just how Rust is meant to be used in practice.

When I try to avoid cloning by passing references everywhere, things often get complicated pretty quickly. I end up dealing with lifetime annotations, or I hit mutable/immutable borrow conflicts, especially when:

  • data is stored inside a struct
  • multiple functions need access to that data
  • I want to mutate one field while reading another

I understand that Rust’s rules exist to guarantee memory safety, but as a learner it’s hard to know where to draw the line between “clean and idiomatic” and “over-engineering to avoid a clone.”

So I’m curious how more experienced Rust developers approach this:

  • Do you generally allow cloning early on and optimize later?
  • Are there clear signs that a clone is actually a problem?
  • How do you decide when to redesign data ownership instead of fighting the borrow checker?

Any guidance, rules of thumb, or real-world examples would be really appreciated.

There is a very simple, founded rule to performance questions: If your program runs fast enough, it's alright.

Inside a struct, the broad rule is: Make it owned, i.e. no references.

References in structs are fine, if the struct is living only for some short amount of time (not wall clock, but control flow related).

That's a suboptimal starting point or direction.

It's not really a question of where to allow cloning and what to optimize, it's more a question of how you design your program's data structures.

In my experience, this is one of those aspects of Rust that I could only learn by doing. Theorizing didn't help much. By building things and hitting walls with my chosen data structures, I learned what works well in Rust and what doesn't.

My suggestion: just keep building (a lot of) things and learn as you go. It takes a little bit time and practice.

3 Likes

I plan the flow of data through my program relatively early on at this point. Split the immutable and mutable data apart, and to try to keep it in as few long-lived places as possible (to avoid having scattered references to it, which could make borrowck an issue). I don’t frequently end up fighting the borrow checker. (And the few times I do, it’s lack of view types (see below) when I have something which is genuinely just a grab-bag of random data and references… which isn’t exactly ideal code to begin with. Or Polonius returns, which will be supported by the next borrow checker.)

It’s probably just some sort of intuition that comes from writing a lot of stuff in Rust. You can get there eventually.

My solution, when I want a method that does something like that (and if, for the sake of “encapsulation” or some sort of “this is simpler to write right now” justification, I accept the ugliness of doing it), is to make an associated method that splits self into each of the precise fields I need. Like, Self::foo(&mut self.field1, &self.field2, .., arg1, arg2). Additionally, I make a macro_rules! macro to make calling it more convenient (e.g. foo!(self, arg1, arg2)). This is, essentially, a mimicry of an idea called “views” and “view types”.

For (what I think are) obvious reasons, I use that strategy for internal / private functionality, and only sparingly even there. I wouldn’t want my public-facing API to be that ugly.

If avoiding a clone is hard, then yes. I suspect it’s perfectly fine to leave “hard” as relative. For me, “hard” is somewhere in the ballpark of “avoiding this clone requires using self-referential structs and a boatload of unsafe, and that’d take too many hours of work”. I do try to avoid most clones from the onset, though.

However, besides clones, the more general problem is avoiding allocations and reusing resources. Drawing the line on how much time to spend on using and providing stuff like Vec::with_capacity, providing methods that reset structs (and reuse allocations), returning resources to a pool on Drop, etc, is the harder decision for me. “Optimize later” is presumably acceptable there. It’s so easy to want to overengineer those problems.

1 Like

Yes, absolutely.

Here's the simple version that doesn't run into trouble:

You can always change to fancier things later once you're more comfortable, but TBH you usually don't need it.

TL/DR:

2 Likes

One particular case I'll call out is returning errors. Mixing borrowing errors with early returns can both be a pain locally, and also has to bottom out somewhere in the call stack, where the borrowed resource lives. For these reasons my error types tend to not borrow even if borrowing is technically possible.

There may be the rare exception where I expect encountering errors and recovering from them to happen often.[1] But usually errors are considered an exceptional case where performance isn't much of a concern.


  1. Parsing that needs to look ahead? ↩︎

1 Like

It's my approach, otherwise you will develop in Rust forever. The red flag could be a case when you clone without further modification of the cloned value.