Skip to content

← Performance and Data Layout step 3 of 20

Easy Primitives

Where the allocations are: the counting allocator

Measure allocator calls for four ways of building vectors and strings. Fix the starter so each construction gets its own measurement, rather than including the counts from earlier constructions.

pub fn alloc_report(n: usize) -> Vec<usize>

What the counter measures

The harness supplies alloc_reset() and alloc_count(). Its global allocator increments one counter for each call to alloc, alloc_zeroed or realloc, then delegates the request to the system allocator. Deallocation does not increment or decrement the counter.

This counts allocation requests, including reallocations. It does not count bytes, live allocations, elapsed time or physical memory copies. A realloc call counts even when the allocator can grow the block at the same address.

The counter is shared by the whole process. Resetting it excludes earlier work such as parsing input, but does not isolate you from unrelated work that happens during a measurement. Keep logging, report allocation and other setup outside the measured regions. Do not spawn background work.

Four separate measurements

Keep the supplied PARTS array and these four constructions unchanged. Each uses the same n. Return their counts in this order:

Index Construction
0 Push n u64 values into a Vec that starts with no capacity.
1 Push the same values into a Vec created with capacity n.
2 Rebuild a String with format on every iteration.
3 Append the same parts into a String created with capacity n * 5.

The vector values are 0..n, converted to u64. Both string loops append PARTS[i % 5] for each i in 0..n, where PARTS is ["alpha", "beta", "gamma", "delta", "eps"].

Create the four-entry report buffer before the first reset. For each construction, reset immediately before it, read the count immediately after it, then store that count. Keep the supplied black_box(&value) calls so the completed values are exposed to an optimization barrier.

An empty construction with n = 0 produces zero in each slot in the current fixtures. The report's own storage is outside those measurements. Inputs must be small enough for n * 5 and the requested buffers to fit; allocation failure and arithmetic overflow are outside this exercise's input domain.

Reading the results

The current fixtures expect [9, 1, 1999, 1] for n = 1000. These counts were reproduced with Rust 1.98.1 in debug and optimized builds. They describe this implementation and setup, not a portable allocation guarantee from Rust.

  • The growing Vec makes an initial request, then additional requests as its capacity fills. In that measured implementation, the u64 vector starts with capacity four and doubles until it reaches 1024. Rust guarantees amortized constant-time push, not that exact growth sequence.
  • The preallocated Vec has enough capacity for all n pushes. It avoids growth requests during this loop; the values still have to be written.
  • format!("{}{}", joined, part) creates a new String containing the old contents followed by the next part. Repeatedly rebuilding an ever-longer prefix does quadratic total copying work for these fixed-size parts. The observed request count also depends on formatting's capacity choices.
  • The preallocated String has enough space because every part is at most five bytes. push_str appends without rebuilding the previous prefix. Both string constructions produce 4400 bytes when n is 1000.

Fewer allocator calls do not translate directly into the same factor of speedup. Counts help explain one source of work; timings and profiling answer different questions.

Measurement limits

Not every Box, Vec or String construction makes a heap allocation. Empty buffers, zero-sized values, existing capacity and compiler optimizations can avoid allocation. A global allocator also does not observe memory managed directly by a separate allocator or foreign library.

black_box is a best-effort optimization barrier, not a guarantee that every source-level allocation occurs. Compiler, standard-library and target changes can change the observed counts. This exercise currently compares exact counts, so it is sensitive to those changes. Measure the supplied operations; do not replace them with formulas that merely reproduce the fixtures.

The starter resets only once, so later entries accumulate earlier counts. Fix that measurement boundary while preserving the four constructions. Submissions must compile, pass the output checks and pass Clippy. The output checks cannot prove that an implementation actually performed the measurements.