Skip to content

← Performance and Data Layout step 8 of 20

Medium End-to-End

Store and transpose a grid in one flat buffer

Store a rectangular grid in one Vec<f32> and transpose it without growing the output buffer repeatedly. The starter produces the right values, but its repeated pushes exceed the allocation budget for the large test.

Keep these public fields and implement the supplied methods:

pub struct Grid {
    pub data: Vec<f32>,
    pub cols: usize,
}
Method Required behavior
new(rows: usize, cols: usize) -> Grid Store (r * 1000 + c) as f32 at each zero-based coordinate (r, c), in row order.
rows(&self) -> usize Return data.len() / cols, or zero when cols is zero.
get(&self, r: usize, c: usize) -> f32 Return the stored value at (r, c).
row(&self, r: usize) -> &[f32] Borrow the entire row without copying or allocating.
transpose(&self) -> Grid Return an independent grid whose (c, r) equals the original's (r, c). Leave the original unchanged.

The grader also constructs a grid directly, so preserve the names and types of both fields. get, row and transpose must use the stored data, not recompute the pattern used by new.

For example, transposing data = [1, 2, 3, 4, 5, 6] with cols = 3 gives data = [1, 4, 2, 5, 3, 6] with cols = 2. The original has two rows; the result has three.

Index a flat grid

Each row occupies cols consecutive elements. Element (r, c) is at r * cols + c, and row r is the slice from r * cols up to, but not including, (r + 1) * cols.

Supplied nonempty grids have a positive column count and a data length divisible by it. Calls to get and row use valid coordinates. Dimensions, index arithmetic and the integer expression in new fit in usize, and buffers fit in memory. Converting a large integer to f32 can round it; preserve the specified cast rather than changing the value formula.

An empty grid has no valid element or row to read. The two-field representation cannot retain a nonzero row count when cols is zero: new(3, 0).rows() is zero. Transpose uses the row count reported by rows() as its new cols.

Reserve space before filling

The harness counts calls to alloc, alloc_zeroed and realloc separately around construction and transpose. Each operation must use at most one counted call, including for the 1000 by 1000 test.

Allocate enough space for the whole output before writing it. Two useful patterns are Vec::with_capacity(rows * cols) followed by pushes, and vec![0.0; rows * cols] followed by indexed writes. Reserving capacity does not set the vector's length, so indexing an unfilled reserved vector panics.

Growing from Vec::new() can require several reallocations. Rust does not promise a particular growth factor, and a reallocation may grow in place rather than copy the buffer. The exercise's counter is a check on this toolchain, not a timing benchmark or a portable guarantee about optimizer behavior. Rust permits allocation calls to be optimized away.

What flat storage changes

A flat vector keeps all stored elements contiguous. A Vec<Vec<f32>> stores a vector header for each row, with each nonempty row owning its own buffer. A header contains a pointer, length and capacity; it is not just a pointer. The row buffers need not be adjacent to one another.

Nested vectors are useful for rows of different lengths. For a rectangular grid, flat storage avoids separate row buffers and exposes the whole grid as a slice. Both representations can borrow an individual row as &[f32].

Contiguity can help sequential access and vectorization, but it does not guarantee either faster code or SIMD instructions. A column traversal jumps by cols elements in this row-major layout; its cache behavior depends on the stride and hardware. A tiled transpose can improve locality on large grids, but is not required here. Measure representative sizes before choosing a more complicated traversal.

Array of structs or struct of arrays?

For particles with position and mass, an array of structs (AoS) stores records together in a Vec<Particle>. A struct of arrays (SoA) keeps a separate vector for each field, such as x, y, z and mass.

AoS keeps one particle's fields close together, which can help a loop that reads the whole record. SoA puts all x values together, which can help a loop that reads only x. Neither layout wins for every workload.

SoA also needs a way to keep its vectors the same length and their indices aligned. You can still construct a particle value or a view of its fields; the representation simply requires more care to expose that interface.

Your solution must compile, match the outputs, meet the allocation budget and pass Clippy with warnings treated as errors.