Rust / Advanced / Lesson 17
Profiling, allocation, and data layout
Measure equivalent behavior under stated build conditions, then reduce unnecessary allocation or copying without weakening lifetime and domain contracts.
On this page
Lesson goals
- Separate algorithmic scaling from machine-specific measurements.
- Explain vector reservation and conditional ownership with Cow.
- Keep benchmark claims reproducible and statistically honest.
Define the workload before optimizing
A performance claim needs a workload, input distribution, compiler version, target, profile, and measurement method. Begin with a correct reference result and a concrete objective such as reducing median latency or peak memory for that workload. Algorithmic complexity describes growth, while allocation, locality, branches, and I/O influence constants. A debug build does not represent an optimized release configuration, and changing overflow or panic settings can change behavior. Compare equivalent semantics and record variability rather than claiming that an abstraction is universally zero-cost or that one container is always faster.
Reserve known capacity without assuming layout
The first example knows its final count and reserves capacity before appending squares. This can avoid repeated growth allocations, but it does not create elements or guarantee an exact capacity. The output is a correctness result, not a fabricated timing measurement. For real code, compare with and without reservation using representative sizes and include allocation failure policy where needed. Reserving a huge speculative count can waste memory and increase failure risk. Vec provides contiguous elements, but the struct layout of an arbitrary Rust type is not a stable external binary representation.
fn main() {
let mut squares = Vec::with_capacity(4);
for value in 1u64..=4 { squares.push(value * value); }
let total: u64 = squares.iter().sum();
println!("{} {total}", squares.len());
}
4 30
The sequence is one, four, nine, sixteen. Its four values sum to thirty; exact capacity and runtime are not promised.
Borrow unchanged data and own transformed data
Cow can represent either borrowed or owned data behind one interface. The second example has an explicit ASCII-only lowercasing policy. If no uppercase ASCII byte exists, it returns the input borrow; otherwise it returns an owned lowercase String. This avoids allocating an unchanged result while preserving ownership when transformation is required. The borrowed alternative still depends on the input lifetime, and into_owned can materialize an independent value when needed. Do not call ASCII conversion a complete Unicode case-folding algorithm. A performance optimization must not silently change text semantics or acceptance rules.
use std::borrow::Cow;
fn lower_ascii(text: &str) -> Cow<'_, str> {
if text.bytes().all(|byte| !byte.is_ascii_uppercase()) {
Cow::Borrowed(text)
} else {
Cow::Owned(text.to_ascii_lowercase())
}
}
fn main() {
for input in ["rust", "RUST"] {
let output = lower_ascii(input);
println!("{output} {}", matches!(&output, Cow::Borrowed(_)));
}
}
rust true
rust false
The lowercase input stays borrowed. The uppercase input needs an owned conversion, so its borrowed flag is false.
Measure work the optimizer cannot simply remove
A microbenchmark must use its inputs and outputs in a way that prevents measuring an eliminated computation. std::hint::black_box can help as a best-effort optimization barrier, but it is not a security guarantee or a replacement for inspecting benchmark structure. Use enough repetitions, control setup work, and report whether allocation and I/O are included. Instant measures elapsed duration with platform characteristics; do not present one local duration as a guaranteed program output. External profilers require supported operating-system tooling. Standard stable Rust does not imply every benchmark framework or profiler is installed.
Keep proofs while changing representation
Borrowing slices can reduce copying, but a queued consumer may need owned storage. Flattening data can improve locality but alter update costs or identity relationships. Generics may allow inlining while increasing binary size; dynamic dispatch may be appropriate when extension and build-time costs matter. Profile the actual bottleneck and verify the same regression suite before accepting a change. Unsafe unchecked indexing should not be a reflexive response to an unmeasured bounds-check concern. Document the measured assumptions and retain an understandable reference implementation when it helps detect semantic drift. Correctness and resource limits remain constraints on every optimization.
Common errors and fixes
A faster version returns different results.
- Why it happens
- The benchmark compared different semantics.
- How to fix it
- Restore the contract before evaluating speed.
- Prevent it next time
- Require correctness checks around every candidate.
A borrowed Cow result escapes its source.
- Why it happens
- Conditional ownership was treated as guaranteed ownership.
- How to fix it
- Use into_owned or retain the source appropriately.
- Prevent it next time
- Document both alternatives in the lifetime contract.
A tiny benchmark measures almost no work.
- Why it happens
- Optimization removed unused computations or setup dominated.
- How to fix it
- Preserve observable results and separate setup from work.
- Prevent it next time
- Review generated behavior and repeat representative cases.
Check your understanding
1. For lower_ascii("Rust Lab"), give the text and ownership alternative. Explain why the borrowed alternative cannot be returned for that transformation.
Show worked solution
The text is rust lab in Cow::Owned, because at least one uppercase ASCII byte needs replacement. A shared &str cannot modify the caller text, and a borrow of a new local String would expire on return. Owning the transformed String keeps the result valid independently while the unchanged path can still avoid an allocation.
2. A profiler shows allocation dominates a report that rebuilds a temporary vector on every call. Propose one optimization and the evidence needed to accept it.
Show worked solution
Reuse a caller-owned scratch buffer or reserve a realistic known bound while clearing old elements between calls. Verify identical report values, no retained references across mutation, and acceptable peak capacity. Measure representative repeated calls including the chosen reuse policy. A lower allocation count alone is insufficient if memory retention or output latency becomes worse.
Key takeaways
- Measure a stated workload with equivalent semantics.
- Allocation avoidance must preserve ownership and text policy.
- Keep optimization evidence and correctness checks together.