In this lesson, you will learn how to weigh the CPU cost of duplicating data against the architectural complexity and performance bottlenecks introduced by long-lived references.
The Hidden Cost of Copying
In Rust, types that implement the Copy trait are duplicated bit-by-bit when assigned or passed to functions. For primitive types like i32 or bool, this is incredibly fast—often just a single instruction. However, as data structures grow, copying becomes a performance liability because the CPU must spend time moving bytes and potentially evicting useful data from your CPU cache.
The Price of Avoiding the Copy
To avoid copying, we often reach for references (&T). While references avoid duplicating data, they introduce two distinct performance costs:
- Indirection: A reference is a pointer. To access the data, the CPU must perform a "dereference" operation, which can cause a cache miss if the data isn't nearby.
- Lifetime Constraints: Long-lived references force the borrow checker to keep the underlying data alive. This can prevent the compiler from optimizing memory layout or reusing space, effectively "pinning" data in memory longer than necessary.
A Practical Comparison
Consider a scenario where you have a small struct.
#[derive(Clone, Copy)]
struct Point { x: f64, y: f64 }
fn process_point(p: Point) { /* Copy happens here */ }
fn process_ref(p: &Point) { /* No copy, but adds indirection */ }
If Point is small, passing by value (copying) is usually faster because the data stays in CPU registers. If you use a reference, you force the data onto the stack or heap, requiring the CPU to fetch it through a memory address.
However, if you had a LargeData struct (e.g., an array of 1000 items), copying it on every function call would be disastrous for performance. Here, the "risk" of a long-lived reference—the complexity of managing lifetimes—is a trade-off you must accept to keep the application responsive.
The Decision Matrix
- Copy: Use for small, fixed-size data. It keeps data local and cache-friendly, avoiding the overhead of the borrow checker.
- Reference: Use for large, complex, or long-lived data. Use it to avoid the high latency of moving large memory blocks, but accept that you are introducing pointer indirection and strict lifetime requirements.
Key Takeaway: Prefer copying small data to keep it in registers, but use references to avoid the latency of moving large memory buffers, even if it adds architectural complexity.