Clone Cost Stratification
Clone Cost Stratification
Rust spells every Clone::clone call the same way: .clone(). The syntax is identical whether you're bumping an Arc<T> reference count (~2 ns, no allocation) or deep-copying a Vec<Vec<HashMap<String, String>>> (potentially microseconds and many allocations). The type system gives no warning. This opacity is the single biggest footgun for high-level Rust code — the entire style depends on cloning generously, and a deep clone on a hot path can convert a 10–30% acceptable overhead into a 10× regression.
Why this matters now
In a GC'd language, "make a copy" usually means a shallow reference copy and the runtime amortizes the rest. In Rust, .clone() recursively walks the value: String clones the heap buffer, Vec clones each element, HashMap rebuilds the table. There is no aliasing optimization, no copy-on-write, no shared backing store unless you explicitly opted in. For a Rust newcomer adopting a cloning-as-default style, the first profiler session is a shock.
The strata
Roughly, in increasing cost order:
| Tier | Example | Cost | Notes |
|---|---|---|---|
| Free | Copy types (u64, &T, small POD structs) |
0 ns | Compiler-elided memcpy |
| Cheap | Arc<T>, Rc<T>, Bytes |
~2 ns | Atomic increment, no allocation |
| Cheap | Arc<str>, Arc<[T]> |
~2 ns | Same as above, immutable shared slice |
| Cheap | im::Vector, im::HashMap, im::OrdMap |
log(n) | Structural sharing via champ-style tries |
| Moderate | Small owned String, Vec<T> of POD |
~100 ns | One allocation + memcpy |
| Expensive | Vec<String>, nested collections |
µs–ms | Allocation per element |
| Catastrophic | Large nested maps/vectors in a loop | unbounded | The footgun |
The high-level-rust discipline is to ensure every .clone() you write lives in the first three tiers.
Mitigations
Arc<T> for shared ownership
The default carrier for any non-trivial owned value in high-level Rust. Wrap large structs and collections in Arc at the boundary; clones become reference bumps; mutation becomes either Arc::make_mut (CoW) or replacement with a new Arc.
Immutable collection crates
For collections you actually need to update — but rarely, or where the cost of full replacement would dominate — use a persistent collection. The CHAMP family (Compressed Hash-Array Mapped Prefix-tree) gives O(log n) updates with structural sharing, the same trick that makes Clojure and Scala's immutable collections viable. The Rust ecosystem's im crate is the canonical implementation; rpds and archery are alternatives. See persistent-functional-list for the rank-1 case and persistent-queues for the queue family.
Bytes for binary data
bytes is the Arc<[u8]> of the tokio ecosystem: zero-copy cheap clones, slice operations that don't copy, used pervasively in hyper, axum, and tonic. If you're holding bytes in a high-level Rust app, hold them in Bytes.
Cow<T> for conditional ownership
The borrow → Cow → owned hierarchy stays relevant. Cow<str> is the right return type when 90% of inputs pass through unchanged — most clones become borrows.
Arc<dyn Trait> for service composition
The pillar of high-level-rust's third recommendation: hold trait-object services behind Arc and clone them freely to whatever code needs the dependency.
The LightClone idea
The essay author proposes a crate called LightClone that would re-introduce the cheap/expensive distinction at the type level. The mechanism (sketched, not yet implemented as of the essay): a marker trait LightClone implemented for cheap types only, plus lints or generic bounds that flag deep clones in code paths annotated as hot. The proposal sits in a long line of "Rust needs a clone-aliasing story" discussions — claim, Cheap, and various Owned/Shared distinctions in older RFC threads — but none has shipped. The underlying ask is real: the cost gap between tiers spans 5+ orders of magnitude and the language gives no help in distinguishing them.
Related ergonomics work: the cargo-mutants family of analyzers, Clippy's expect_used/clone_on_ref_ptr lints (which catch a narrow slice of the problem), and the broader "ergonomics" RFC discussion around implicit cloning of Arc-like types.
The bigger pattern
This is one face of a recurring Rust theme: the language exposes cost honestly but doesn't stratify it lexically. The same critique applies to:
await— looks like a function call, may not yield, may yield for millisecondsdyn Trait— looks like a call, may inline (rare), almost always vtables- generic monomorphization — looks like one function, may produce dozens of copies and balloon binary size
- macro invocation — looks like a call, may expand to hundreds of lines
For systems-level code, honest costs are the point; you measure and respond. For high-level code, unstratified costs are the friction the style is most exposed to. Naming the tiers — even informally, as in the table above — is the cheapest mitigation.
Related
- high-level-rust — the style that depends on this discipline
- rust-allocation-patterns — borrow → Cow → owned; the expert-side counterpart
- bytes — the canonical cheap-clone binary buffer
- champ, persistent-functional-list — the structurally-shared immutable collection family
- functional-core-imperative-shell — the style that produces the most clones in normal code
Linked from
Sources
- Raw/Rust/High-Level Rust: Getting 80% of the Benefits with 20% of the Pain.md