Skip to content

← Performance and Data Layout step 18 of 20

Medium End-to-End

Reserve output space and report iterator size hints

Avoid repeated output-buffer growth in two tasks: concatenating rows and collecting values from a run-length iterator. The starter produces the right values, but it does not meet the size-hint and allocation requirements.

1. Concatenate the rows

pub fn flatten_rows(rows: &[Vec<i32>]) -> Vec<i32>

Return every value in row order, preserving duplicates and negative values. Rows may have different lengths or be empty. An empty input, or an input of only empty rows, produces an empty vector. Leave the input unchanged.

For example, [[2, -1], [], [2, 7]] becomes [2, -1, 2, 7].

Reserve enough capacity for the sum of the row lengths, then append each row. extend_from_slice is useful here. rows.concat() is another option. The total length fits in usize and the output fits in memory.

The starter's rows.iter().flatten().copied().collect() has the right output, but does not reserve the full combined length on the tested toolchain. The number of rows alone cannot tell Flatten how many values the rows contain. It does not scan all those row lengths just to report its initial size hint.

2. Report the remaining run length

Complete the Iterator implementation for RunLength, constructed by:

pub fn run_length(runs: Vec<(i32, usize)>) -> RunLength

Each pair is (value, count). For example, [(7, 3), (9, 2)] yields 7, 7, 7, 9, 9. Skip zero-count runs wherever they appear. An empty list or all-zero counts yields no values. Values may be any i32; the sum of counts fits in usize, and the decoded output fits in memory.

The supplied next method already tracks remaining. Implement size_hint so it returns (remaining, Some(remaining)) without consuming any values. It must stay exact after every successful next call and return (0, Some(0)) after exhaustion.

For [(7, 3), (9, 2)], the initial hint is (5, Some(5)). After taking three values it is (2, Some(2)), and collecting the rest gives [9, 9].

What a size hint promises

Iterator::size_hint returns (lower, upper). The lower bound cannot exceed the number of remaining values; Some(upper) cannot be smaller than that number. None means no upper bound is supplied, not that the iterator is necessarily infinite. The default (0, None) is valid but gives little information for reserving an output buffer.

A loose bound is allowed by the trait. This exercise deliberately requires exact bounds because the decoder already knows the remaining count. A wrong bound is a logic bug, but safe iterator consumers must not rely on it for memory safety. Returning an exact hint does not automatically implement ExactSizeIterator; that separate, optional trait exposes len() and is not required here.

This complete example shows that the hint describes values still to come:

fn main() {
    let mut values = [4, 8, 12].into_iter();
    assert_eq!(values.size_hint(), (3, Some(3)));
    assert_eq!(values.next(), Some(4));
    assert_eq!(values.size_hint(), (2, Some(2)));
    assert_eq!(values.collect::<Vec<_>>(), vec![8, 12]);
}

Allocation checks

The harness allows at most one call to alloc, alloc_zeroed or realloc during flatten_rows, and at most one while collecting the decoder's remaining values. It reads the decoder hint before and after attempting to take three values. If fewer than three exist, the iterator is exhausted. The large fixture flattens 2000 rows of 500 values and decodes 100,000 values.

A useful size hint lets a vector reserve space before filling. The precise strategies used by collect and extend are implementation details; they are not universally a call to with_capacity(size_hint().0). A vector also has no guaranteed doubling policy, and growing its allocation does not necessarily copy the existing data.

The allocation counter checks this exercise on the current toolchain. It is not a timing measurement or a portable promise that every allocation call will occur: Rust permits the optimizer to eliminate allocations.

Iterator syntax alone does not determine performance. An adapter with an inexact length can still compile into an efficient loop, especially when no output collection is needed. Reserve or precompute a length when it helps the actual workload; benchmark before assuming a manual loop is faster.

Your solution must compile, match the values and exact hints, meet the allocation limits and pass Clippy with warnings treated as errors.