201. or_insert vs or_insert_with — Don't Build a Default You'll Throw Away

map.entry(k).or_insert(expensive()) builds expensive() on every call — even when the key is already there and the value gets dropped on the floor. Reach for or_insert_with and the default is computed only when it’s actually needed.

The entry API already saves you the contains_key-then-insert double lookup. But there’s a second, quieter cost hiding in or_insert: its argument is an ordinary value, so it’s evaluated before the call, regardless of whether the slot is occupied or vacant.

1
2
3
4
5
6
7
8
9
use std::collections::HashMap;

let mut cache: HashMap<&str, String> = HashMap::new();
cache.insert("hit", "already here".to_string());

// "expensive default".to_string() allocates a fresh String here...
// ...then gets immediately discarded because "hit" is occupied.
cache.entry("hit").or_insert("expensive default".to_string());
assert_eq!(cache["hit"], "already here");

That throwaway allocation happens on every hit. In a hot loop over a mostly-populated map, you’re paying to construct defaults you never store.

or_insert_with takes a closure instead of a value, so the work is deferred until the entry is genuinely vacant:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::collections::HashMap;

let mut map: HashMap<&str, String> = HashMap::new();
map.insert("a", "existing".to_string());

let mut builds = 0;

// "a" is present, so the closure never runs.
map.entry("a").or_insert_with(|| { builds += 1; "default".to_string() });
assert_eq!(builds, 0);

// "b" is vacant, so the closure runs exactly once.
map.entry("b").or_insert_with(|| { builds += 1; "default".to_string() });
assert_eq!(builds, 1);

assert_eq!(map["a"], "existing");
assert_eq!(map["b"], "default");

The rule of thumb: if the default is a plain literal or a cheap Copy value (0, false, None), or_insert is fine and reads cleaner. The moment the default allocates or computes — a Vec::new(), a String, a hash of the key, a database handle — switch to or_insert_with.

When the default depends on the key itself, or_insert_with_key hands the key to the closure so you don’t have to capture it:

1
2
3
4
5
6
7
8
9
use std::collections::HashMap;

let mut sizes: HashMap<String, usize> = HashMap::new();

let n = sizes
    .entry("hello".to_string())
    .or_insert_with_key(|k| k.len());

assert_eq!(*n, 5);

All three still cost a single hash and a single lookup — the entry lands on the slot once and hands you a &mut V. The only thing you’re choosing is when the default gets built: always, or only when it’s needed.

#200 Jun 2026

200. #[derive(Copy)] and #[inline] — Make Small Types Free to Pass Around

A two-field struct that you .clone() everywhere, behind a function the optimizer won’t inline across crates — that’s two small taxes you can stop paying. Copy deletes the move/drop bookkeeping, #[inline] lets the body fold into the caller.

This is the afternoon half of a pair with the morning’s static-dispatch bite: both are about handing the optimizer a body it can actually see through and fold into the caller.

Copy: tiny types shouldn’t need moving

For a small, plain-data struct — a couple of integers, a pair of floats — a move is just a memcpy. But without Copy, the value is moved out of its binding when you pass it, so you can’t use it again afterward, and the compiler tracks drop state for it. Derive Copy and it’s duplicated bit-for-bit instead, no move semantics, no drop glue:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#[derive(Copy, Clone, Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn shift(p: Point, dx: i32) -> Point {
    Point { x: p.x + dx, y: p.y }
}

let a = Point { x: 1, y: 2 };
let b = shift(a, 10);
// `a` is still usable — it was copied, not moved
assert_eq!(a, Point { x: 1, y: 2 });
assert_eq!(b, Point { x: 11, y: 2 });

Without Copy that shift(a, ..) would move a, and the later assert_eq!(a, ..) wouldn’t compile. You’d reach for .clone() or & borrows to work around it — friction for a type that’s cheaper to copy than a pointer.

The rule of thumb: derive Copy when the type is small (fits in a register or two) and has no heap-owning fields. String, Vec, and Box can’t be Copy — only Clone — because duplicating them bit-for-bit would alias an owned allocation.

#[inline]: the optimizer can’t see across crate walls

Within a single crate the compiler inlines freely based on its own cost model. The catch is the crate boundary: a normal (non-generic) function compiled into your library is just a symbol other crates call into. Without LTO, the calling crate only sees the signature, not the body — so it emits a real call and the optimizer can’t fold a one-line helper into the loop around it.

#[inline] ships the function’s body in the crate metadata so downstream crates can inline it:

1
2
3
4
5
6
#[inline]
pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
    a + (b - a) * t
}

assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);

This matters for exactly the small, hot, public functions where the call overhead rivals the work: accessors, newtype getters, math helpers, Iterator glue. Generic functions and Copy-type constructors are already inlinable across crates (their code is monomorphized at the call site), so you mostly need #[inline] for concrete, non-generic ones.

#[inline(always)] is the stronger hammer — it overrides the cost model. Reach for it rarely, only for trivial wrappers you’ve measured, because over-inlining bloats code size and can evict the instruction cache.

Putting both on a newtype wrapper

The combination shows up constantly on zero-cost newtypes — make the value free to pass and free to call through:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#[derive(Copy, Clone, Debug, PartialEq)]
struct Meters(f64);

impl Meters {
    #[inline]
    fn as_feet(self) -> f64 {
        self.0 * 3.28084
    }
}

let d = Meters(100.0);
let ft = d.as_feet();
// `d` is Copy, so taking `self` by value didn't consume it
assert_eq!(d, Meters(100.0));
assert!((ft - 328.084).abs() < 1e-9);

as_feet takes self by value with zero guilt because Meters is Copy, and #[inline] means a downstream crate calling .as_feet() in a tight loop gets the multiply folded in directly instead of a function call.

The bottom line

For small plain-data types, #[derive(Copy)] removes move/drop overhead and the ergonomic friction that pushes you toward needless .clone()s. For small public functions, #[inline] gives downstream crates the body to fold into their own code. Both are about the same thing as static dispatch: never make the optimizer guess at something you could just hand it. Profile first — but these two are cheap wins on the hot path.

199. Static Dispatch — Generics Beat Box<dyn Trait> When You Can Afford the Code

Box<dyn Trait> is the reflex when a function “takes something that implements a trait.” But every call through it pays for a vtable hop the compiler can’t see past. Swap it for a generic and the optimizer inlines the whole thing.

This is the morning half of a pair with this afternoon’s #[inline] & Copy bite: both are about giving the optimizer a body it can actually fold into the caller.

The cost: Box<dyn Trait> hides the call from the optimizer

When you accept Box<dyn Fn(i32) -> i32> (or any dyn Trait), the concrete type is erased. At each call site the program loads a function pointer from a vtable and jumps through it. The compiler has no idea what’s on the other end, so it can’t inline the body, can’t constant-fold through it, can’t vectorize a loop around it:

1
2
3
fn apply_all(f: Box<dyn Fn(i32) -> i32>, xs: &[i32]) -> Vec<i32> {
    xs.iter().map(|&x| f(x)).collect() // indirect call every iteration
}

There’s also a heap allocation just to hold the closure, and the pointer-chase ruins instruction-cache locality in a hot loop.

The fix: a generic parameter monomorphizes to the real type

Take impl Fn (sugar for a generic) instead. The compiler stamps out a specialized copy of apply_all for each concrete f you pass — monomorphization. Inside that copy the closure’s body is fully visible, so it gets inlined and the map loop optimizes as if you’d written the arithmetic by hand:

1
2
3
fn apply_all<F: Fn(i32) -> i32>(f: F, xs: &[i32]) -> Vec<i32> {
    xs.iter().map(|&x| f(x)).collect() // direct call, inlinable
}

No box, no vtable, no allocation. impl Trait in argument position is the same thing with less typing:

1
2
3
fn apply_all(f: impl Fn(i32) -> i32, xs: &[i32]) -> Vec<i32> {
    xs.iter().map(|&x| f(x)).collect()
}
1
2
let doubled = apply_all(|x| x * 2, &[1, 2, 3]);
assert_eq!(doubled, vec![2, 4, 6]);

Both generic versions compile to a tight loop with the multiply spliced straight in.

The same trick for returns: impl Trait instead of Box<dyn>

Returning a dyn value forces a box too. If the function only ever returns one concrete type, impl Trait keeps it static — the caller gets the real type and can inline through it:

1
2
3
4
5
6
fn adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n          // one concrete closure type, no Box
}

let add5 = adder(5);
assert_eq!(add5(10), 15);

When dyn is still the right call

Static dispatch trades code size for speed: each instantiation is a fresh copy, so monomorphizing over many types bloats the binary. And generics can’t do heterogeneous collections — Vec<Box<dyn Draw>> holding circles and squares genuinely needs dynamic dispatch, because the element type varies at runtime. Reach for dyn when you need a uniform type for mixed values, want to shrink compile times and binary size, or the call isn’t hot enough to matter. Reach for generics / impl Trait when the call sits in a loop and you want the optimizer to see through it.

1
2
3
4
5
// heterogeneous → dyn is correct
let shapes: Vec<Box<dyn Fn() -> &'static str>> =
    vec![Box::new(|| "circle"), Box::new(|| "square")];
let names: Vec<&str> = shapes.iter().map(|s| s()).collect();
assert_eq!(names, vec!["circle", "square"]);

Rule of thumb: default to a generic, and downgrade to dyn only when you have a reason — mixed types, code-size pressure, or a cold path where the indirection is free.

198. into_iter() to Transform — Move Owned Items Instead of cloned()

You have a Vec you’re about to throw away, and you want a transformed one. Reaching for iter().cloned() (or iter().map(|x| x.clone())) duplicates every element on the way out — but you owned them already. into_iter() moves them straight through.

This is the afternoon half of this morning’s mem::replace bite: both are about moving owned data forward instead of copying it. There, it was an enum behind &mut self; here, it’s the elements of a collection.

The trap: iter().cloned() on a collection you’re discarding

You want to uppercase a list of names. The list is a local you won’t touch again:

1
2
3
4
5
6
fn shout(names: Vec<String>) -> Vec<String> {
    names
        .iter()                       // yields &String
        .map(|s| s.to_uppercase())    // to_uppercase already allocates a fresh String...
        .collect()
}

That one’s not even the worst case — to_uppercase builds a new String regardless. The real waste shows up when the transform keeps the value and you clone just to own it:

1
2
3
4
5
6
7
fn tag(names: Vec<String>) -> Vec<(String, usize)> {
    names
        .iter()                                  // &String
        .enumerate()
        .map(|(i, s)| (s.clone(), i))            // clone purely to own it
        .collect()
}

Every s.clone() heap-allocates a duplicate of a string you were about to drop. The original names gets freed on return — you paid to copy bytes that were headed for the incinerator.

The fix: into_iter() consumes the collection and hands you owned items

into_iter() on a Vec<String> yields String by value, not &String. The transform now moves each element — no clone, no second allocation:

1
2
3
4
5
6
7
fn tag(names: Vec<String>) -> Vec<(String, usize)> {
    names
        .into_iter()                 // yields String, owned
        .enumerate()
        .map(|(i, s)| (s, i))        // move s straight in
        .collect()
}
1
2
3
let names = vec!["ada".to_string(), "linus".to_string()];
let tagged = tag(names);
assert_eq!(tagged, vec![("ada".to_string(), 0), ("linus".to_string(), 1)]);

Each string’s heap buffer is threaded through by pointer. Zero element copies.

The rule of thumb

If you still need the collection afterward, iter() (borrow) is correct — you can’t move out of something you’re keeping. But the moment the collection is yours to consume and you don’t need it again, into_iter() skips a copy of every element. A for x in v loop already does this (it’s into_iter under the hood); the win is remembering that .map, .filter, and friends can start from into_iter() too.

1
2
3
4
5
6
// keep the source → borrow
let total: usize = names.iter().map(|s| s.len()).sum();
println!("{}", names.len()); // still usable

// done with the source → move
let owned: Vec<String> = names.into_iter().filter(|s| s.len() > 3).collect();

cloned() earns its keep when you genuinely need both the original and a copy. When you don’t, it’s a tax on data you’re about to free.

197. Advance a State Machine with mem::replace — Move the Enum Out, No Clone

Transitioning an enum state behind &mut self looks impossible: you can’t move the old variant’s owned data into the new one without the borrow checker stopping you — so people reach for .clone(). mem::replace lets you move the whole state out, leaving a cheap placeholder behind.

This closes out the performance week. Earlier bites covered mem::take, mem::replace, and mem::swap as primitives. Here’s the pattern they were built for: a state machine that moves owned data forward through its transitions.

The setup

A job that walks Queued → Running → Done, carrying an owned String payload from one state into the next:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#[derive(Debug, PartialEq)]
enum Stage {
    Queued { payload: String },
    Running { payload: String, worker: u32 },
    Done { result: String },
}

struct Job {
    stage: Stage,
}

The trap: matching a borrow forces a clone

You only have &mut self, so the obvious move is to match on &self.stage. But that gives you a borrow of payload — to put it in the next state you have to clone it:

1
2
3
4
5
6
7
8
9
fn advance(&mut self) {
    self.stage = match &self.stage {
        Stage::Queued { payload } => Stage::Running {
            payload: payload.clone(), // borrowed, so clone to reuse
            worker: 7,
        },
        // ...
    };
}

Matching on self.stage by value would move out of &mut self — the borrow checker rejects it outright. So clone feels like the only way out. It isn’t.

The fix: replace the whole state, then match by value

mem::replace swaps in a cheap placeholder and hands you the real state by value. Now the match owns payload and can move it straight into the next variant — zero clones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::mem;

impl Job {
    fn advance(&mut self) {
        self.stage = match mem::replace(&mut self.stage, Stage::Done { result: String::new() }) {
            Stage::Queued { payload } => Stage::Running { payload, worker: 7 },
            Stage::Running { payload, worker } => {
                Stage::Done { result: format!("{payload}@{worker}") }
            }
            done => done, // terminal state stays put
        };
    }
}

The placeholder (Done { result: String::new() }) is free — an empty String allocates nothing — and it lives for only the instant before you overwrite self.stage with the match result.

1
2
3
4
5
6
7
let mut job = Job { stage: Stage::Queued { payload: "build".into() } };

job.advance();
assert_eq!(job.stage, Stage::Running { payload: "build".into(), worker: 7 });

job.advance();
assert_eq!(job.stage, Stage::Done { result: "build@7".into() });

The payload string is allocated once and threaded through all three states by pointer. No copy of the bytes ever happens — exactly what the clone-based version threw away on every transition.

196. Return impl Iterator, Not Vec — Let the Caller Decide What to Do

Returning a Vec from a helper allocates eagerly, every time — even when the caller only wants the first match or a running sum. Return impl Iterator instead and the allocation simply never happens unless the caller asks for it.

This is the function-boundary version of yesterday’s bite-195: chaining adapters avoids temporary Vecs inside a pipeline; returning impl Iterator avoids forcing one across a function call.

The eager version

A helper that builds and returns a Vec commits to a heap allocation and a full pass over the data before the caller has said what they want:

1
2
3
fn evens_doubled(nums: &[i32]) -> Vec<i32> {
    nums.iter().filter(|&&n| n % 2 == 0).map(|&n| n * 2).collect()
}

If the caller just wants the first result, they still pay for the whole Vec:

1
2
3
let data = [1, 2, 3, 4, 5, 6];
let first = evens_doubled(&data).into_iter().next(); // allocated all 3, used 1
assert_eq!(first, Some(4));

Hand back the iterator instead

Drop the .collect() and return the lazy iterator. The + '_ ties its lifetime to the borrowed slice:

1
2
3
fn evens_doubled(nums: &[i32]) -> impl Iterator<Item = i32> + '_ {
    nums.iter().filter(|&&n| n % 2 == 0).map(|&n| n * 2)
}

Now nothing runs until the caller pulls values through — and they pick the consumer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let data = [1, 2, 3, 4, 5, 6];

let v: Vec<i32> = evens_doubled(&data).collect(); // collect if you want to
assert_eq!(v, [4, 8, 12]);

let total: i32 = evens_doubled(&data).sum();       // or fold straight to a number
assert_eq!(total, 24);

let first_big = evens_doubled(&data).find(|&n| n > 5); // or short-circuit
assert_eq!(first_big, Some(8));                    // stops at 8, never doubles 6

The find call never allocates and never touches the last element. The Vec-returning version couldn’t do that — collect() always drains the whole thing first.

The one rule: don’t borrow a local

The iterator you return can borrow your parameters, but not data you created inside the function — that data is dropped when the function ends. Iterators over owned values (like a Range) carry no borrow, so they just work:

1
2
3
4
5
6
fn squares(n: u32) -> impl Iterator<Item = u32> {
    (1..=n).map(|x| x * x)
}

let sq: Vec<u32> = squares(4).collect();
assert_eq!(sq, [1, 4, 9, 16]);

If you must produce owned data inside the function and stream it out, move it into the iterator (e.g. vec.into_iter() or a move closure) rather than returning a borrow of a local.

195. Chain Iterator Adapters — Don't collect() Between Every Step

Every collect::<Vec<_>>() in the middle of a pipeline is a heap allocation and a full pass over your data. Adapters like map and filter are lazy and fuse together — chain them and the whole transformation runs in one pass with zero temporary Vecs.

A Vec between every step

It’s tempting to do one transformation at a time, binding each result to a variable. Every collect() allocates a throwaway Vec and walks the entire sequence before the next step even starts:

1
2
3
4
5
6
7
let nums = [1, 2, 3, 4, 5, 6];

let doubled: Vec<i32> = nums.iter().map(|&n| n * 2).collect();
let big: Vec<i32> = doubled.into_iter().filter(|&n| n % 4 == 0).collect();
let sum: i32 = big.iter().sum();

assert_eq!(sum, 24);

Two intermediate Vecs, two extra allocations, three separate passes — all to compute a single number.

One chain, one pass, no temporaries

The adapters compose directly. Nothing is materialized until the final consumer (sum) pulls values through, so there are no intermediate collections at all:

1
2
3
4
5
6
7
8
9
let nums = [1, 2, 3, 4, 5, 6];

let sum: i32 = nums
    .iter()
    .map(|&n| n * 2)        // 2, 4, 6, 8, 10, 12
    .filter(|&n| n % 4 == 0) // 4, 8, 12
    .sum();                  // 24

assert_eq!(sum, 24);

Each element flows through map then filter then into the sum, one at a time. No buffer is ever allocated.

Laziness means short-circuiting works

Because nothing runs until pulled, a chain only does the work it needs. Add a take(2) and the pipeline stops after producing two results — the elements past that point are never touched:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::cell::Cell;

let visited = Cell::new(0);
let nums = [1, 2, 3, 4, 5, 6, 7, 8];

let first_two: Vec<i32> = nums
    .iter()
    .inspect(|_| visited.set(visited.get() + 1))
    .map(|&n| n * 10)
    .filter(|&n| n > 20)
    .take(2)
    .collect();

assert_eq!(first_two, [30, 40]);
assert_eq!(visited.get(), 4); // stopped early — never looked at 5..8

The intermediate-collect version can’t do this: collect() always drains the whole iterator, so it would have visited all eight elements before take ever saw one.

When you genuinely do need a Vec

The point isn’t “never collect” — it’s “don’t collect between steps.” Collect once, at the end, when you actually need an owned, reusable collection:

1
2
3
4
5
6
7
8
9
let words = ["fast", "lazy", "fused", "iter"];

let shouted: Vec<String> = words
    .iter()
    .filter(|w| w.len() == 4)
    .map(|w| w.to_uppercase())
    .collect();

assert_eq!(shouted, ["FAST", "LAZY", "ITER"]);

One collect, at the end, when the Vec is the actual result. Everything before it stays lazy and allocation-free.

194. Reuse One Buffer with .clear() — Allocate Once, Loop Many Times

with_capacity (bite 193) buys a buffer once instead of growing it repeatedly. But if you allocate a fresh String or Vec inside a loop, you throw that buffer away every iteration. .clear() resets the length to zero while keeping the capacity — so one allocation serves the whole loop.

A fresh allocation every iteration

It’s easy to declare the working buffer inside the loop. Each pass allocates a new heap buffer and drops it at the end of the iteration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let lines = ["alpha", "beta", "gamma"];
let mut out = Vec::new();

for line in lines {
    let mut buf = String::new();   // new heap allocation, every iteration
    buf.push_str(line);
    buf.make_ascii_uppercase();
    out.push(buf.clone());
}

assert_eq!(out, ["ALPHA", "BETA", "GAMMA"]);

Three iterations, three allocate-then-free cycles for the scratch buffer. Scale that to a million lines and it’s a million wasted allocations.

.clear() keeps the capacity

Hoist the buffer out of the loop and clear() it at the top of each pass. clear() sets the length to 0 but leaves the allocated capacity in place, so after the first iteration the buffer is already big enough and never reallocates:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let lines = ["alpha", "beta", "gamma"];
let mut out = Vec::new();
let mut buf = String::new();       // allocated once

for line in lines {
    buf.clear();                   // len -> 0, capacity untouched
    buf.push_str(line);
    buf.make_ascii_uppercase();
    out.push(buf.clone());
}

assert_eq!(out, ["ALPHA", "BETA", "GAMMA"]);

The contract is the whole point — clear drops the contents but not the buffer:

1
2
3
4
5
6
7
let mut s = String::with_capacity(64);
s.push_str("hello");
let cap = s.capacity();

s.clear();
assert_eq!(s.len(), 0);            // empty again
assert_eq!(s.capacity(), cap);     // ...but the buffer is still there

The read-into-a-reused-buffer pattern

This shows up constantly when reading input. BufRead::read_line appends to the buffer you give it, so the idiomatic loop clears one String each pass instead of allocating a new one per line:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::io::BufRead;

let input = "12\n34\n56\n";
let mut reader = std::io::BufReader::new(input.as_bytes());

let mut line = String::new();      // one buffer for every line
let mut sum = 0i64;

loop {
    line.clear();                  // required — read_line appends
    let n = reader.read_line(&mut line).unwrap();
    if n == 0 {
        break;                     // 0 bytes read == EOF
    }
    sum += line.trim().parse::<i64>().unwrap();
}

assert_eq!(sum, 102);

The same trick works for any scratch Vecclear() it at the top of the loop and reuse the capacity for the next batch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let mut scratch: Vec<u8> = Vec::new();
let mut total = 0;

for chunk in [&[1u8, 2, 3][..], &[4, 5], &[6]] {
    scratch.clear();
    scratch.extend_from_slice(chunk);
    total += scratch.iter().map(|&b| b as u32).sum::<u32>();
}

assert_eq!(total, 21);

Reach for a fresh Vec/String only when you actually need to keep each result. When the buffer is just scratch space, allocate it once, clear() it, and let the loop run free.

193. Vec::with_capacity — Size Up Front, Skip the Realloc Churn

A Vec you push into one element at a time doesn’t grow one element at a time — it doubles, copying every existing item to a fresh allocation each time it outgrows its buffer. If you already know how many items are coming, Vec::with_capacity buys the whole buffer once.

The hidden cost of push

Vec::new() starts with zero capacity. As you push, it reallocates geometrically — roughly doubling each time — and every reallocation copies all existing elements into the new, larger buffer. Fill a vector with 1000 items and you pay for that copying around ten times over:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let mut v = Vec::new();
let mut reallocs = 0;
let mut last_cap = v.capacity();

for i in 0..1000 {
    v.push(i);
    if v.capacity() != last_cap {
        reallocs += 1;        // the buffer just moved
        last_cap = v.capacity();
    }
}
// ~10 reallocations, each copying everything built so far
assert!(reallocs >= 8);

In a hot loop, that churn is pure waste: allocate, copy, free, allocate bigger, copy again.

Reserve the space once

If you know the final size, hand it to Vec::with_capacity. The buffer is allocated a single time, and push never has to move it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let mut v = Vec::with_capacity(1000);
let mut reallocs = 0;
let mut last_cap = v.capacity();

for i in 0..1000 {
    v.push(i);
    if v.capacity() != last_cap {
        reallocs += 1;
        last_cap = v.capacity();
    }
}
assert_eq!(reallocs, 0);       // zero — the buffer never moved

Capacity is not length: with_capacity(1000) gives you room for 1000 items but the vector is still empty (len() == 0) until you push.

Already have a Vec? Use reserve

When the vector exists and you’re about to add a known number of elements, reserve grows the buffer ahead of time without touching the contents:

1
2
3
4
5
let mut v = vec![1, 2, 3];
v.reserve(100);               // ensure room for 100 *more*

assert!(v.capacity() >= 103);
assert_eq!(v.len(), 3);       // still 3 elements — only capacity changed

Use reserve before a batch of pushes; use reserve_exact when you want the buffer sized precisely, with no geometric slack.

collect often does this for you

Iterators expose a size_hint, so collecting from a sized iterator already reserves the right capacity — no manual call needed:

1
2
let squares: Vec<i32> = (0..1000).map(|x| x * x).collect();
assert_eq!(squares.len(), 1000);

The win is biggest exactly where it matters: tight loops building large vectors. If you can name the size, name it once and let push run free.

192. impl Into<String> — Take Owned or Borrowed Without an Extra Allocation

Bite 191 said: if you only read the argument, take &str. But what if you need to store it? Taking &str and calling .to_owned() always allocates — even when the caller handed you a String it was about to throw away. impl Into<String> fixes that.

The hidden re-allocation

When a function keeps the value, the “take &str” rule turns into a trap:

1
2
3
4
5
struct Label { text: String }

fn make_label(text: &str) -> Label {
    Label { text: text.to_owned() } // always allocates
}

A literal caller has to allocate eventually — fair enough. But look what happens when the caller already owns a String:

1
2
3
4
let owned = String::from("Status: OK");
let label = make_label(&owned);
// `owned` is copied into a brand-new allocation, then `owned` is dropped.
// We threw away a perfectly good String and allocated a second one.

The caller had an owned buffer it no longer needed, and we ignored it.

Accept anything that becomes a String

Take impl Into<String>. A String moves in with zero copying; a &str allocates exactly once — never more:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Label { text: String }

fn make_label(text: impl Into<String>) -> Label {
    Label { text: text.into() }
}

// Literal: one allocation, unavoidable since we store it.
let a = make_label("Status: OK");

// Owned String: MOVED in. No copy, no second allocation.
let owned = String::from("Status: OK");
let b = make_label(owned);

assert_eq!(a.text, "Status: OK");
assert_eq!(b.text, "Status: OK");

Same call site for both, and the owned case is now free. The conversion happens lazily at the boundary, exactly once, and only when it must.

When you only read: impl AsRef

If you don’t store the value but still want to accept more than deref coercion allows (String, &str, Box<str>, Cow<str>, …), reach for impl AsRef<str>:

1
2
3
4
5
6
7
fn shout(text: impl AsRef<str>) -> String {
    text.as_ref().to_uppercase()
}

assert_eq!(shout("hi"), "HI");                       // &str
assert_eq!(shout(String::from("hi")), "HI");          // String
assert_eq!(shout(Box::<str>::from("hi")), "HI");      // Box<str>

as_ref() is a cheap borrow — no allocation — and the generic accepts every string-like type without forcing the caller to convert first.

The rule of thumb

If the function stores the string, take impl Into<String> so an owned argument moves in for free. If it only reads but you want maximum flexibility, take impl AsRef<str>. Plain &str (bite 191) is still the right default for simple read-only functions — these two just cover the cases it can’t.