Allocation

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.