.. /index

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:

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

Linked from

Sources