Stack Allocation Patterns
Tutorial
The Problem
Heap allocation via malloc/free (or Box/Vec in Rust) requires a system call or
at minimum a lock-protected free-list traversal, typically costing 50โ200 ns per
allocation. On the stack, allocation is a single sub-instruction decrement of the stack
pointer: effectively free. For small, fixed-size buffers known at compile time, stack
allocation is 10โ100ร faster and produces data already in L1 cache.
Stack allocation matters in three domains: (1) embedded and real-time systems where heap allocators may be absent or non-deterministic, (2) hot loops where allocation cost accumulates (parsing, rendering, physics), (3) cryptographic primitives where heap-allocated secrets leave traces in deallocated memory. The cost of stack allocation is inflexibility: the size must be known at compile time and the data lives only for the current stack frame.
🎯 Learning Outcomes
[T; N] and const N: usize generics for stack buffers[f64; N*M] without heap allocationVec::new() inside loopsCode Example
// [T; N] lives entirely in the stack frame โ zero allocator overhead.
pub fn sum_stack_array() -> f64 {
let data: [f64; 16] = [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
9.0,10.0,11.0,12.0, 13.0, 14.0, 15.0, 16.0,
];
data.iter().copied().sum()
}
// 4ร4 matrix multiply โ 96 bytes on the stack, fits in L1 cache.
pub fn matmul4(a: &[[f32;4];4], b: &[[f32;4];4]) -> [[f32;4];4] {
let mut c = [[0.0f32; 4]; 4];
for i in 0..4 { for k in 0..4 { for j in 0..4 {
c[i][j] += a[i][k] * b[k][j];
}}}
c
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Fixed-size arrays | [T; N] on stack, zero cost | Array.make n x on heap |
| Const generics | const N: usize | Not available |
| Small inline vector | arrayvec, smallvec crates | Manual buffer reuse |
| GC pressure | None (no GC) | Minor heap allocation per call |
| Stack overflow safety | Detectable via OS guard page | Same; default stack 8 MB |
OCaml Approach
OCaml allocates everything (tuples, records, closures, arrays) on its minor heap by
default. The minor GC is fast (copying collector, typically < 1 ยตs), but it still
costs more than stack. There is no direct equivalent of stack allocation except for
int, bool, unit, and unboxed floats in float-only records:
(* OCaml: even a small array goes on the heap *)
let process_events events =
List.fold_left (fun acc e ->
(* This allocates a 16-element array on the minor heap *)
let scratch = Array.make 16 0 in
scratch.(0) <- e;
acc + Array.fold_left (+) 0 scratch
) 0 events
(* Reuse a buffer to reduce allocation pressure *)
let scratch = Array.make 16 0
let process_events_reuse events =
List.fold_left (fun acc e ->
Array.fill scratch 0 16 0;
scratch.(0) <- e;
acc + Array.fold_left (+) 0 scratch
) 0 events
OCaml 5 adds unboxed arrays for numeric types (experimental), which would enable true stack-style allocation.
Full Source
#![allow(clippy::all)]
//! # Stack Allocation Patterns
//!
//! Demonstrates how to keep small, fixed-size data on the stack to avoid
//! heap allocator overhead, gain automatic cleanup, and maximise L1-cache
//! locality.
// โโ Fixed-size stack arrays โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// Sum a fixed-size array that lives entirely in the stack frame.
/// No allocator call, no pointer indirection.
pub fn sum_stack_array() -> f64 {
let data: [f64; 16] = [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
];
data.iter().copied().sum()
}
/// 4ร4 matrix multiply โ all 96 floats live on the stack.
/// No allocator, no pointer chasing; everything fits in L1 cache.
pub fn matmul4(a: &[[f32; 4]; 4], b: &[[f32; 4]; 4]) -> [[f32; 4]; 4] {
let mut c = [[0.0f32; 4]; 4];
for i in 0..4 {
for k in 0..4 {
for j in 0..4 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
// โโ Inline string buffer (stack-allocated) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// A fixed-capacity string buffer backed by a stack array.
///
/// Holds up to `CAP` bytes of UTF-8. No heap allocation whatsoever.
/// Push ASCII bytes; `as_str()` returns a `&str` slice of the filled portion.
pub struct InlineStr<const CAP: usize> {
buf: [u8; CAP],
len: usize,
}
impl<const CAP: usize> InlineStr<CAP> {
pub const fn new() -> Self {
Self {
buf: [0u8; CAP],
len: 0,
}
}
/// Append a string slice. Returns `false` if there is not enough room.
pub fn push_str(&mut self, s: &str) -> bool {
let bytes = s.as_bytes();
if self.len + bytes.len() > CAP {
return false;
}
self.buf[self.len..self.len + bytes.len()].copy_from_slice(bytes);
self.len += bytes.len();
true
}
/// View the filled portion as a `&str`.
pub fn as_str(&self) -> &str {
// Safety: we only accept valid UTF-8 through `push_str`.
std::str::from_utf8(&self.buf[..self.len]).expect("always valid UTF-8")
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn capacity(&self) -> usize {
CAP
}
}
impl<const CAP: usize> Default for InlineStr<CAP> {
fn default() -> Self {
Self::new()
}
}
// โโ ArrayVec โ stack-backed push/pop collection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// A fixed-capacity vector whose storage is a stack-allocated array.
///
/// Provides `push` / `pop` / `as_slice` without any heap allocation.
/// Useful when you need to accumulate a small, bounded number of items.
pub struct ArrayVec<T, const CAP: usize> {
// `MaybeUninit` lets us avoid requiring `T: Default` or `T: Copy`.
data: [std::mem::MaybeUninit<T>; CAP],
len: usize,
}
impl<T, const CAP: usize> ArrayVec<T, CAP> {
pub fn new() -> Self {
Self {
// SAFETY: an array of `MaybeUninit` is always safe to initialise
// this way โ we never read uninitialised slots.
data: unsafe { std::mem::MaybeUninit::uninit().assume_init() },
len: 0,
}
}
/// Push an element. Returns `Err(value)` if capacity is exhausted.
pub fn push(&mut self, value: T) -> Result<(), T> {
if self.len == CAP {
return Err(value);
}
self.data[self.len].write(value);
self.len += 1;
Ok(())
}
/// Pop the last element, if any.
pub fn pop(&mut self) -> Option<T> {
if self.len == 0 {
return None;
}
self.len -= 1;
// SAFETY: slot `self.len` was initialised by a prior `push`.
Some(unsafe { self.data[self.len].assume_init_read() })
}
pub fn as_slice(&self) -> &[T] {
// SAFETY: the first `self.len` slots are initialised.
unsafe { std::slice::from_raw_parts(self.data.as_ptr().cast::<T>(), self.len) }
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn capacity(&self) -> usize {
CAP
}
}
impl<T, const CAP: usize> Default for ArrayVec<T, CAP> {
fn default() -> Self {
Self::new()
}
}
impl<T, const CAP: usize> Drop for ArrayVec<T, CAP> {
fn drop(&mut self) {
// Drop every initialised element to avoid leaking resources.
for slot in &mut self.data[..self.len] {
// SAFETY: slots 0..len are initialised.
unsafe { slot.assume_init_drop() };
}
}
}
// โโ Stack-local ring buffer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// A fixed-capacity ring buffer backed by a stack array.
///
/// When full, the oldest element is overwritten (lossy, like a hardware FIFO).
pub struct RingBuf<T: Copy + Default, const CAP: usize> {
buf: [T; CAP],
head: usize, // index of the next write slot
count: usize,
}
impl<T: Copy + Default, const CAP: usize> RingBuf<T, CAP> {
pub fn new() -> Self {
Self {
buf: [T::default(); CAP], // `Default` required so we can init the array
head: 0,
count: 0,
}
}
/// Write one element, overwriting the oldest if full.
pub fn push(&mut self, value: T) {
self.buf[self.head % CAP] = value;
self.head = (self.head + 1) % CAP;
self.count = (self.count + 1).min(CAP);
}
/// Collect the current contents in insertion order (oldest first).
pub fn to_vec(&self) -> Vec<T> {
if self.count < CAP || self.head == 0 {
self.buf[..self.count].to_vec()
} else {
let mut out = Vec::with_capacity(CAP);
out.extend_from_slice(&self.buf[self.head..]);
out.extend_from_slice(&self.buf[..self.head]);
out
}
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
}
impl<T: Copy + Default, const CAP: usize> Default for RingBuf<T, CAP> {
fn default() -> Self {
Self::new()
}
}
// โโ const-fn new for InlineStr (pure const, no Default call) โ already const โโ
// โโ Tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[cfg(test)]
mod tests {
use super::*;
// โโ sum_stack_array โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[test]
fn test_sum_stack_array() {
// 1 + 2 + โฆ + 16 = 136
assert_eq!(sum_stack_array(), 136.0);
}
// โโ matmul4 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[test]
fn test_matmul4_identity() {
let id = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0f32],
];
let a = [
[1.0, 2.0, 3.0, 4.0],
[5.0, 6.0, 7.0, 8.0],
[9.0, 10.0, 11.0, 12.0],
[13.0, 14.0, 15.0, 16.0f32],
];
assert_eq!(matmul4(&a, &id), a);
assert_eq!(matmul4(&id, &a), a);
}
#[test]
fn test_matmul4_zero() {
let zero = [[0.0f32; 4]; 4];
let a = [[1.0f32; 4]; 4];
assert_eq!(matmul4(&a, &zero), zero);
}
// โโ InlineStr โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[test]
fn test_inline_str_basic() {
let mut s = InlineStr::<32>::new();
assert!(s.is_empty());
assert!(s.push_str("hello, "));
assert!(s.push_str("world"));
assert_eq!(s.as_str(), "hello, world");
assert_eq!(s.len(), 12);
}
#[test]
fn test_inline_str_overflow_rejected() {
let mut s = InlineStr::<8>::new();
assert!(s.push_str("12345678")); // exactly 8 bytes โ fits
assert!(!s.push_str("x")); // one more โ rejected
assert_eq!(s.as_str(), "12345678");
}
#[test]
fn test_inline_str_empty() {
let s = InlineStr::<16>::new();
assert!(s.is_empty());
assert_eq!(s.as_str(), "");
assert_eq!(s.capacity(), 16);
}
// โโ ArrayVec โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[test]
fn test_arrayvec_push_pop() {
let mut v = ArrayVec::<i32, 4>::new();
assert!(v.push(10).is_ok());
assert!(v.push(20).is_ok());
assert!(v.push(30).is_ok());
assert_eq!(v.len(), 3);
assert_eq!(v.as_slice(), &[10, 20, 30]);
assert_eq!(v.pop(), Some(30));
assert_eq!(v.pop(), Some(20));
assert_eq!(v.len(), 1);
}
#[test]
fn test_arrayvec_capacity_enforced() {
let mut v = ArrayVec::<u8, 2>::new();
assert!(v.push(1).is_ok());
assert!(v.push(2).is_ok());
assert_eq!(v.push(3), Err(3)); // full โ rejected
assert_eq!(v.len(), 2);
}
#[test]
fn test_arrayvec_empty_pop() {
let mut v = ArrayVec::<String, 4>::new();
assert_eq!(v.pop(), None);
assert!(v.is_empty());
}
#[test]
fn test_arrayvec_drops_elements() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let counter = Arc::new(AtomicUsize::new(0));
struct Counted(Arc<AtomicUsize>);
impl Drop for Counted {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
{
let mut v = ArrayVec::<Counted, 4>::new();
let _ = v.push(Counted(Arc::clone(&counter)));
let _ = v.push(Counted(Arc::clone(&counter)));
// v drops here
}
assert_eq!(counter.load(Ordering::Relaxed), 2);
}
// โโ RingBuf โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[test]
fn test_ringbuf_basic() {
let mut rb = RingBuf::<i32, 4>::new();
rb.push(1);
rb.push(2);
rb.push(3);
assert_eq!(rb.len(), 3);
assert_eq!(rb.to_vec(), vec![1, 2, 3]);
}
#[test]
fn test_ringbuf_wraps_around() {
let mut rb = RingBuf::<i32, 3>::new();
rb.push(1);
rb.push(2);
rb.push(3);
rb.push(4); // overwrites 1
// Oldest remaining is 2
let v = rb.to_vec();
assert_eq!(v.len(), 3);
assert!(v.contains(&2));
assert!(v.contains(&3));
assert!(v.contains(&4));
assert!(!v.contains(&1));
}
#[test]
fn test_ringbuf_empty() {
let rb = RingBuf::<u8, 8>::new();
assert!(rb.is_empty());
assert_eq!(rb.to_vec(), Vec::<u8>::new());
}
}#[cfg(test)]
mod tests {
use super::*;
// โโ sum_stack_array โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[test]
fn test_sum_stack_array() {
// 1 + 2 + โฆ + 16 = 136
assert_eq!(sum_stack_array(), 136.0);
}
// โโ matmul4 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[test]
fn test_matmul4_identity() {
let id = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0f32],
];
let a = [
[1.0, 2.0, 3.0, 4.0],
[5.0, 6.0, 7.0, 8.0],
[9.0, 10.0, 11.0, 12.0],
[13.0, 14.0, 15.0, 16.0f32],
];
assert_eq!(matmul4(&a, &id), a);
assert_eq!(matmul4(&id, &a), a);
}
#[test]
fn test_matmul4_zero() {
let zero = [[0.0f32; 4]; 4];
let a = [[1.0f32; 4]; 4];
assert_eq!(matmul4(&a, &zero), zero);
}
// โโ InlineStr โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[test]
fn test_inline_str_basic() {
let mut s = InlineStr::<32>::new();
assert!(s.is_empty());
assert!(s.push_str("hello, "));
assert!(s.push_str("world"));
assert_eq!(s.as_str(), "hello, world");
assert_eq!(s.len(), 12);
}
#[test]
fn test_inline_str_overflow_rejected() {
let mut s = InlineStr::<8>::new();
assert!(s.push_str("12345678")); // exactly 8 bytes โ fits
assert!(!s.push_str("x")); // one more โ rejected
assert_eq!(s.as_str(), "12345678");
}
#[test]
fn test_inline_str_empty() {
let s = InlineStr::<16>::new();
assert!(s.is_empty());
assert_eq!(s.as_str(), "");
assert_eq!(s.capacity(), 16);
}
// โโ ArrayVec โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[test]
fn test_arrayvec_push_pop() {
let mut v = ArrayVec::<i32, 4>::new();
assert!(v.push(10).is_ok());
assert!(v.push(20).is_ok());
assert!(v.push(30).is_ok());
assert_eq!(v.len(), 3);
assert_eq!(v.as_slice(), &[10, 20, 30]);
assert_eq!(v.pop(), Some(30));
assert_eq!(v.pop(), Some(20));
assert_eq!(v.len(), 1);
}
#[test]
fn test_arrayvec_capacity_enforced() {
let mut v = ArrayVec::<u8, 2>::new();
assert!(v.push(1).is_ok());
assert!(v.push(2).is_ok());
assert_eq!(v.push(3), Err(3)); // full โ rejected
assert_eq!(v.len(), 2);
}
#[test]
fn test_arrayvec_empty_pop() {
let mut v = ArrayVec::<String, 4>::new();
assert_eq!(v.pop(), None);
assert!(v.is_empty());
}
#[test]
fn test_arrayvec_drops_elements() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let counter = Arc::new(AtomicUsize::new(0));
struct Counted(Arc<AtomicUsize>);
impl Drop for Counted {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
{
let mut v = ArrayVec::<Counted, 4>::new();
let _ = v.push(Counted(Arc::clone(&counter)));
let _ = v.push(Counted(Arc::clone(&counter)));
// v drops here
}
assert_eq!(counter.load(Ordering::Relaxed), 2);
}
// โโ RingBuf โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[test]
fn test_ringbuf_basic() {
let mut rb = RingBuf::<i32, 4>::new();
rb.push(1);
rb.push(2);
rb.push(3);
assert_eq!(rb.len(), 3);
assert_eq!(rb.to_vec(), vec![1, 2, 3]);
}
#[test]
fn test_ringbuf_wraps_around() {
let mut rb = RingBuf::<i32, 3>::new();
rb.push(1);
rb.push(2);
rb.push(3);
rb.push(4); // overwrites 1
// Oldest remaining is 2
let v = rb.to_vec();
assert_eq!(v.len(), 3);
assert!(v.contains(&2));
assert!(v.contains(&3));
assert!(v.contains(&4));
assert!(!v.contains(&1));
}
#[test]
fn test_ringbuf_empty() {
let rb = RingBuf::<u8, 8>::new();
assert!(rb.is_empty());
assert_eq!(rb.to_vec(), Vec::<u8>::new());
}
}
Deep Comparison
OCaml vs Rust: Stack Allocation Patterns
Side-by-Side Code
OCaml
(* OCaml: local integers and floats are unboxed on the stack.
Arrays and most compound values are heap-allocated by the GC. *)
let stack_int_ops () =
let a = 42 in
let b = 100 in
a + b (* no allocation โ unboxed stack ints *)
(* Float arrays get a special unboxed optimisation in OCaml *)
let unboxed_float_array () =
let arr = Array.init 8 (fun i -> float_of_int i *. 1.5) in
Array.fold_left ( +. ) 0.0 arr
(* No language-level fixed-capacity stack vector exists;
the closest is a plain array with a manual length counter. *)
let arrayvec_sim () =
let buf = Array.make 4 0 in
let len = ref 0 in
buf.(!len) <- 10; incr len;
buf.(!len) <- 20; incr len;
(buf, !len)
Rust (idiomatic โ fixed-size array)
// [T; N] lives entirely in the stack frame โ zero allocator overhead.
pub fn sum_stack_array() -> f64 {
let data: [f64; 16] = [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
9.0,10.0,11.0,12.0, 13.0, 14.0, 15.0, 16.0,
];
data.iter().copied().sum()
}
// 4ร4 matrix multiply โ 96 bytes on the stack, fits in L1 cache.
pub fn matmul4(a: &[[f32;4];4], b: &[[f32;4];4]) -> [[f32;4];4] {
let mut c = [[0.0f32; 4]; 4];
for i in 0..4 { for k in 0..4 { for j in 0..4 {
c[i][j] += a[i][k] * b[k][j];
}}}
c
}
Rust (functional โ inline string buffer, no heap)
pub struct InlineStr<const CAP: usize> {
buf: [u8; CAP],
len: usize,
}
impl<const CAP: usize> InlineStr<CAP> {
pub const fn new() -> Self { Self { buf: [0u8; CAP], len: 0 } }
pub fn push_str(&mut self, s: &str) -> bool {
let bytes = s.as_bytes();
if self.len + bytes.len() > CAP { return false; }
self.buf[self.len..self.len + bytes.len()].copy_from_slice(bytes);
self.len += bytes.len();
true
}
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.buf[..self.len]).expect("valid UTF-8")
}
}
Rust (ArrayVec โ stack-backed push/pop)
pub struct ArrayVec<T, const CAP: usize> {
data: [std::mem::MaybeUninit<T>; CAP],
len: usize,
}
impl<T, const CAP: usize> ArrayVec<T, CAP> {
pub fn push(&mut self, value: T) -> Result<(), T> {
if self.len == CAP { return Err(value); }
self.data[self.len].write(value);
self.len += 1;
Ok(())
}
pub fn pop(&mut self) -> Option<T> {
if self.len == 0 { return None; }
self.len -= 1;
Some(unsafe { self.data[self.len].assume_init_read() })
}
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Fixed-size array | int array (heap via GC) | [T; N] (stack frame) |
| Inline string | no native type | InlineStr<CAP> ([u8; CAP] + usize) |
| Stack vector | manual array + ref counter | ArrayVec<T, CAP> with MaybeUninit |
| Matrix | float array array (boxed) | [[f32; 4]; 4] (flat stack block) |
| Capacity failure | runtime exception | Err(T) return value |
Key Insights
[T; N] is always stack-allocated when used as a local variable, giving the programmer explicit control.MaybeUninit enables safe, un-defaulted stack storage.** ArrayVec uses [MaybeUninit<T>; CAP] so T need not implement Default. OCaml arrays always require an initial fill value (Array.make n v), which forces a heap write per element.InlineStr<32> and ArrayVec<i32, 8> carry their capacity as a compile-time constant, enabling zero-cost fixed-size storage while preserving type safety. OCaml has no equivalent โ array sizes are runtime values.Drop is deterministic.** Rust's ArrayVec::drop runs immediately when the variable goes out of scope, calling each element's destructor in turn. OCaml relies on GC finalisation, which may never run or may run much later.[[f32; 4]; 4] is 64 contiguous bytes โ a single cache line. The equivalent OCaml float array array is a boxed array of pointers to boxed inner arrays, scattering data across the heap and causing multiple cache misses per row access.When to Use Each Style
**Use [T; N] (stack array) when:** the size is known at compile time and small (rule of thumb: under 4 KB). Examples: fixed-size buffers, small matrices, SIMD-friendly data.
**Use ArrayVec<T, CAP> when:** you need push/pop semantics but the maximum number of elements is statically bounded and small. Examples: accumulating results in an inner loop without heap allocation, building small lists in embedded or latency-sensitive code.
**Use Vec<T> / String (heap) when:** size is dynamic, potentially large, or you need to return the collection from a function without copying โ the heap is the right tool for unbounded growth.
Exercises
Matrix<3, 3> multiplication using only stack-allocated [f64; 9] arrays and benchmark it vs Vec<f64>-backed multiplication for 1M iterations.
arrayvec crate to implement a StackVec<T, 16> that stores up to 16elements on the stack and panics on overflow. Write unit tests for push/pop/iter.
&[f32] window of size up to 32 using [f32; 32] and insertion sort.
process_events with heap vs stack scratch buffer using heaptrack.Quantify the allocation reduction and wall-clock improvement.
stacker::maybe_grow for recursive algorithms that risk stack overflow,and implement a recursive Fibonacci with dynamic stack growth.