ExamplesBy LevelBy TopicLearning Paths
725 Intermediate

Stack Allocation Patterns

Functional Programming

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

  • โ€ข Understand the stack frame model and why stack allocation is free
  • โ€ข Use fixed-size arrays [T; N] and const N: usize generics for stack buffers
  • โ€ข Implement matrix operations over [f64; N*M] without heap allocation
  • โ€ข Apply stack allocation in hot loops by avoiding Vec::new() inside loops
  • โ€ข Recognize stack overflow risk and safe maximum stack sizes (~1โ€“8 MB on most systems)
  • Code 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

    AspectRustOCaml
    Fixed-size arrays[T; N] on stack, zero costArray.make n x on heap
    Const genericsconst N: usizeNot available
    Small inline vectorarrayvec, smallvec cratesManual buffer reuse
    GC pressureNone (no GC)Minor heap allocation per call
    Stack overflow safetyDetectable via OS guard pageSame; 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());
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Fixed-size arrayint array (heap via GC)[T; N] (stack frame)
    Inline stringno native typeInlineStr<CAP> ([u8; CAP] + usize)
    Stack vectormanual array + ref counterArrayVec<T, CAP> with MaybeUninit
    Matrixfloat array array (boxed)[[f32; 4]; 4] (flat stack block)
    Capacity failureruntime exceptionErr(T) return value

    Key Insights

  • Stack vs GC heap. OCaml allocates almost everything on its GC heap; the compiler only keeps small unboxed values (int, bool, float in local position) on the stack. Rust's [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.
  • Const generics encode capacity in the type. 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.
  • Cache locality. A 4ร—4 [[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

  • Implement Matrix<3, 3> multiplication using only stack-allocated [f64; 9]
  • arrays and benchmark it vs Vec<f64>-backed multiplication for 1M iterations.

  • Use the arrayvec crate to implement a StackVec<T, 16> that stores up to 16
  • elements on the stack and panics on overflow. Write unit tests for push/pop/iter.

  • Implement a stack-allocated median filter for a &[f32] window of size up to 32
  • using [f32; 32] and insertion sort.

  • Profile process_events with heap vs stack scratch buffer using heaptrack.
  • Quantify the allocation reduction and wall-clock improvement.

  • Investigate stacker::maybe_grow for recursive algorithms that risk stack overflow,
  • and implement a recursive Fibonacci with dynamic stack growth.

    Open Source Repos