8.6 The Cost of Clone and When to Avoid It
Alright, let’s talk about Clone. It’s the “I need one of those too” trait. You see an object, you want an exact replica, you call .clone(). Simple, right? The problem is, it’s a siren song. It’s so easy to use that it becomes the go-to solution for any ownership problem, and that’s how you end up with a codebase that’s slower than a snail on sedatives.
The fundamental thing you must internalize is this: Clone is explicit, but not necessarily cheap. Unlike the Copy trait, which is a blind, cheap bit-for-bit duplication happening implicitly behind the scenes, Clone is an explicit method call. You are asking for a duplication. And what happens inside that method is entirely up to the type’s implementation. For a String or a Vec, it means allocating a whole new chunk of memory and copying every single byte over. That’s an O(n) operation. Do that in a tight loop and you’ve just built a performance bottleneck.
The Performance Hit is Real
Let’s make this concrete. Consider this seemingly innocent code.
fn process_data(data: &Vec<String>) -> Vec<String> {
let mut results = Vec::new();
for item in data {
// Oops, a clone in a loop!
let cloned_item = item.clone();
results.push(do_expensive_work(cloned_item));
}
results
}
Every single iteration of that loop involves a new heap allocation for the cloned String and a copy of all its contents. If data contains 10,000 strings that are each 1KB, you’ve just asked for 10MB of allocations and 10MB of data copying. The CPU is out there sweating while you just casually call clone(). The worst part? This is almost certainly unnecessary. The function takes a reference &Vec<String>, so we probably didn’t even need ownership in the first place.
When You Absolutely Should Not Clone
This is the golden rule: Don’t clone to fix a borrow checker error you don’t understand.
The borrow checker isn’t your enemy; it’s a brilliant, if sometimes pedantic, friend preventing you from shooting yourself in the foot. When it tells you you can’t move out of a reference, your first instinct might be to just .clone() the value and move the clone instead. Stop. This is a design smell. 99 times out of 100, the correct solution is to rethink your ownership model.
You’re working with a shared reference (
&T): If you only have a shared reference, you can’t get ownership of the originalT. Cloning seems tempting. But ask yourself: does the function you’re calling really need ownership? Could it just take a reference&Tinstead? Often, the answer is yes. Forcing yourself to work with references leads to better, more efficient APIs.You need a subset of the data: Cloning a massive struct just to access one field is a cardinal sin.
struct UserProfile { id: u64, name: String, // ...20 more fields, including a `Vec<PurchaseHistory>` } fn get_user_name(profile: &UserProfile) -> &str { // GOOD: Borrow just the bit you need. &profile.name } fn get_user_name_bad(profile: &UserProfile) -> String { // BAD: Allocates a new String for no reason. profile.name.clone() }The good version returns a slice borrowed from the original
String, with zero copying. The bad version performs a completely pointless allocation and deep copy.
The Right Way: Rc and Arc for Shared Ownership
Sometimes, you genuinely need multiple parts of your code to have ownership of the same data. This is where Clone starts to make sense, but not on the data itself—on the pointer to the data.
Enter Rc<T> (for single-threaded) and Arc<T> (for multi-threaded). These are reference-counted smart pointers. Their secret superpower is that their .clone() implementation is cheap. It doesn’t deep-clone the T inside; it just increments a reference counter and gives you a new pointer to the same allocation.
use std::rc::Rc;
let massive_data = Rc::new(vec![0u8; 10_000_000]); // One big allocation
let owner_1 = Rc::clone(&massive_data); // Cheap
let owner_2 = Rc::clone(&massive_data); // Also cheap
// Both point to the exact same 10MB vector on the heap.
assert!(Rc::ptr_eq(&owner_1, &owner_2));
This is the ideal use case for Clone: duplicating a handle to a shared resource, not the resource itself. The cost is a few atomic operations on the reference counter, which is negligible compared to cloning 10MB of data.
So, the next time your finger hovers over the .clone() method, pause. Ask yourself: “Do I need a whole new independent copy of this data, or do I just need to share access to it?” Your answer will determine whether you’re writing efficient Rust or just brute-forcing your way through the type system.