We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Performance and Data Layout step 12 of 20
Bounds checks: five safe ways to express access
Safe slice access checks that an index or range is valid. The optimizer can remove a runtime check when it can establish that the access stays in bounds. This exercise uses a sliding-window calculation to explore how to express those relationships clearly.
Your task
pub fn convolve1d(signal: &[f64], kernel: &[f64]) -> Vec<f64>
Slide the kernel over the signal without padding. At each position, multiply matching elements and add them from left to right:
out[i] = signal[i] * kernel[0]
+ signal[i + 1] * kernel[1]
+ ...
Start each accumulator at 0.0, visit kernel indices in increasing order,
and perform a multiplication followed by an addition for each term. Do not
reverse the kernel. This is cross-correlation, the convention commonly used
by neural-network convolution layers; the required function name remains
convolve1d.
Return an empty vector if the kernel is empty or longer than the signal.
Otherwise, return signal.len() - kernel.len() + 1 outputs. For example,
signal [1, 2, 3, 4, 5] and kernel [1, 0, -1] produce [-2, -2, -2].
Inputs are finite floating-point values sized to fit in memory and complete
within the time limit.
Floating-point addition is not associative. Preserve the specified order rather than replacing it with a reordered reduction or explicit fused multiply-add. An operation order defines the calculation; it is not a blanket guarantee of identical bits across every compiler and target.
Validate lengths before subtracting
The starter computes signal.len() - k + 1 before checking k.
Subtracting a larger kernel length underflows usize.
With overflow checks enabled, that subtraction panics. With them disabled,
unsigned arithmetic wraps. If the kernel is exactly one element longer,
the subtraction wraps to usize::MAX and the following addition wraps to
zero. That happens to give an empty output. A kernel two elements longer
instead produces a huge loop bound and the first window's indexing panics.
Neither behavior is a valid way to handle the input.
The daemon builds with -O, whose usual defaults disable overflow checks.
They can still be explicitly enabled with -C overflow-checks=yes. Safe
out-of-bounds indexing still panics in an optimized build. See the
rustc code-generation options.
Check for an empty or oversized kernel first. This also fixes the starter's empty-kernel behavior, which produces a vector of zeros rather than an empty vector.
Five useful patterns
These patterns can make valid access easier to see. They do not promise a particular instruction count or speedup.
- Iterate over elements. A slice iterator yields its elements without a user-supplied index. The loop still needs to detect when iteration ends.
- Create a subslice. A window such as
&signal[i..i + k]validates a range and gives later code a smaller slice to work with. Arbitrary indexing into that slice is still checked unless proven safe. - Check a relationship once. If an operation requires equally sized
inputs,
assert_eq!(a.len(), b.len())documents and checks that contract. The optimizer may use the fact to simplify later accesses. - Pair iterators with
zip. It stops at the shorter iterator, so it does not establish equal lengths. In this exercise, each window and the kernel have the same length by construction. - Use
chunks_exactfor complete blocks. Its chunk size can be a runtime value. The final incomplete block is available throughremainder(); it is not yielded as a full chunk. A zero chunk size panics. It does not produce the overlapping windows needed for this task.
This complete example shows the block/remainder distinction:
fn check_blocks(size: usize) {
let values = [1, 2, 3, 4, 5];
let mut blocks = values.chunks_exact(size);
assert_eq!(blocks.next(), Some(&[1, 2][..]));
assert_eq!(blocks.next(), Some(&[3, 4][..]));
assert_eq!(blocks.next(), None);
assert_eq!(blocks.remainder(), &[5]);
}
fn main() {
check_blocks(2);
}
See the slice methods
for these APIs. For the exercise, a window plus zip is enough: validate
the lengths, construct each window, then accumulate its paired products.
Check correctness before measuring
Removing a bounds check does not automatically enable vectorization or improve elapsed time. Floating-point rules, loop structure, memory access, target hardware and compiler settings all matter. Compare equivalent implementations on a stated workload and inspect generated code when you need to know whether a check remains. Similar timings alone do not prove that two implementations generated the same instructions.
The harness reports output length, the first five values and a checksum. Implement the full output vector: those observations are not a proof that every remaining element is correct.
get_unchecked requires its index to be in bounds whenever it is called.
Violating that requirement causes undefined behavior, even if the returned
reference is never read. It is unnecessary here. A safe implementation lets
the compiler optimize access without making correctness depend on an
unchecked index.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.