We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Performance and Data Layout step 3 of 20
Where the allocations are: the counting allocator
Timing is a terrible way to learn about allocation. It is noisy, it depends on your machine, your thermal state and what else is running, and it tells you that something is slow without telling you what.
Allocation counts have none of those problems. They are exact, they are reproducible, they are the same on your laptop and on CI, and every one of them is explainable from first principles. This item introduces the instrument that makes them visible — and that this platform uses to grade several later problems.
How it works
Rust lets you replace the global allocator with your own:
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(l) }
}
// dealloc, realloc, alloc_zeroed likewise
}
#[global_allocator]
static COUNTING: Counting = Counting;
Every Box::new, every Vec growth, every String — everything in the
program — goes through it. This harness installs exactly that, and hands you
two functions:
alloc_reset(); // set the counter to zero
alloc_count() -> usize // read the counter
Note realloc counts too. A Vec growing in place still asks the allocator
for more memory and may still memcpy; calling that “free” would be a lie.
The caveat, stated plainly
The counter is process-global. It is already counting the JSON reader that
parsed your test input, and it will keep counting anything else that runs.
That is exactly why you must alloc_reset() immediately before the thing
you want to measure and alloc_count() immediately after — and why later
problems state allocation budgets as <= with a little slack rather than as
exact equalities.
What to write
pub fn alloc_report(n: usize) -> Vec<usize>
Perform these four constructions, in this order, measuring each one independently, and return the four counts:
// 0
let mut grown: Vec<u64> = Vec::new();
for i in 0..n { grown.push(i as u64); }
// 1
let mut sized: Vec<u64> = Vec::with_capacity(n);
for i in 0..n { sized.push(i as u64); }
// 2
let mut joined = String::new();
for i in 0..n { joined = format!("{}{}", joined, PARTS[i % 5]); }
// 3
let mut built = String::with_capacity(n * 5);
for i in 0..n { built.push_str(PARTS[i % 5]); }
Wrap each finished value in std::hint::black_box before moving on. Without
it the optimiser is entitled to notice you never read grown and delete the
whole loop, and you would measure zero.
The numbers you are about to produce, and why
For n = 1000, the correct answer is [9, 1, 1999, 1].
9. Vec::new() does not allocate at all — an empty Vec is a null-ish
dangling pointer, a zero length and a zero capacity, entirely on the stack.
The first push allocates capacity 4, and each time it fills, Vec doubles:
4, 8, 16, 32, 64, 128, 256, 512, 1024. Nine allocations to hold a thousand
elements. This doubling is what makes push amortised O(1) — each element
is copied on average a constant number of times.
1. with_capacity asks for the whole thing up front. One allocation, no
copying, no doubling. Every subsequent push is a store and a length bump.
1999. This is the one worth staring at. format!("{}{}", joined, part)
does not append to joined — it builds an entirely new String containing
a copy of the old one plus the new part, and then you throw the old one away.
Two allocations per iteration (the new buffer, plus one growth inside the
formatting machinery), minus one for the empty first round. It is also O(n²)
in bytes copied: 6.6 MB moved to produce a 4.4 KB string.
1. with_capacity plus push_str writes into one buffer, in place,
forever. One allocation, 4.4 KB copied. This is the same output as
checkpoint 2, produced with 1999× fewer allocations.
That last comparison is the reason this instrument exists. You did not need a profiler, a stopwatch or a quiet machine to see it.
Related lints
vec_init_then_push (you built a Vec::new() and immediately pushed known
elements — use vec![...]), slow_vector_initialization (you grew a vector
in a loop where vec![0; n] or with_capacity would do), and
repeat_vec_with_capacity. All default-on. None of them catch the format!
accumulation, which is the expensive one — clippy helps least exactly where
it would help most.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.