7.2 Stack Memory: Fast Allocation for Sized, Copy Types
Let’s talk about the stack. It’s not a sexy data structure, but it’s the bedrock of fast execution in most programming languages, and Rust is no exception. Think of it like the prep area in a professional kitchen: it’s meticulously organized, everything has its place, and you work on things in a strict last-in, first-out order. You grab a clean pan (allocate), do your searing (compute), and then the dishwasher immediately whisks it away to be cleaned (deallocate). It’s brutally efficient.
This speed is the entire reason we use it. Allocating memory on the stack is literally just a single instruction: move the stack pointer. That’s it. Deallocating is just as fast; move the pointer back. This happens automatically when a variable goes out of scope. There’s no garbage collector hunting around for unused memory, no complex system calls to the OS. It’s just… done.
But this incredible efficiency comes with two very strict rules. A value on the stack must have a known, fixed size at compile time. And its lifetime is strictly confined to the scope in which it’s declared. The moment you exit that scope—poof. It’s gone. This is the heart of Rust’s automatic memory management for stack data.
The Rules: Sized and Copy
Rust formalizes these stack-friendly rules with two key traits: Sized and Copy. Most primitive types you know and love are Sized and Copy: integers (i32, u64), floats (f64), booleans (bool), and characters (char). The Sized trait is compiler magic that says, “Yep, I know how big this type is right now.” You can’t put something on the stack if you don’t know how much space to reserve for it. That’s why a type like str (without the &) can’t be used directly; a string slice could be any length. We put those on the heap, which we’ll get to later.
The Copy trait is even more interesting. It signifies that a type is so simple and cheap to duplicate that it should be copied rather than moved. This is a crucial distinction that trips up every new Rustacean. Let’s see it in action.
fn main() {
let x = 5; // i32 is Copy
let y = x; // This creates a *copy* of the value in x
println!("x = {}, y = {}", x, y); // This is fine! x is still valid.
let s = String::from("hello"); // String is NOT Copy
let t = s; // This *moves* the value from s to t. s is now invalid.
// println!("s = {}", s); // This would cause a compile-time error!
println!("t = {}", t); // This is perfectly fine.
}
See the difference? With a Copy type like i32, assigning y = x does a simple bit-for-bit copy. Both x and y are independent, valid values. But String, which manages a heap-allocated buffer, is not Copy. When we write let t = s, we’re moving ownership. The data itself isn’t necessarily copied (though it might be, the compiler is smart), but the responsibility for cleaning it up is transferred. The variable s is effectively dead to us. This prevents a dreaded “double free” error.
The Perils of Assuming Copy
The biggest pitfall here is assuming your custom type is Copy by default. It is absolutely not. Rust is conservative. Your type only gets the Copy trait if all of its constituent parts are also Copy. You have to explicitly opt-in with the #[derive(Copy, Clone)] attribute.
// This is a perfectly fine Copy type. It's just a bag of integers.
#[derive(Copy, Clone, Debug)]
struct Point {
x: i32,
y: i32,
}
// This will NOT compile. String is not Copy, so MetaData can't be either.
#[derive(Copy, Clone)]
struct MetaData {
id: u64,
name: String, // Nope. Absolutely not. Can't do it.
}
Trying to derive Copy on MetaData will result in a very clear compiler error telling you exactly what’s wrong. It’s one of those moments where the compiler isn’t just being pedantic; it’s saving you from catastrophic memory unsafety. If MetaData were Copy, you’d end up with two variables both thinking they own the same String, and both would try to free its memory. Kaboom.
Best Practices and When to Use It
The rule of thumb is simple: favor stack allocation for any data that is small, fixed-size, and cheap to copy. Your function’s intermediate values, primitive types, and simple structs like Point should live on the stack. It’s the fastest, safest way to manage memory.
The moment your data becomes large or of unknown size, you’ve outgrown the stack’s neat little prep area. You need to graduate to the heap, which is the whole chaotic warehouse of memory. But that’s a story for the next section. For now, just appreciate the stack for what it is: a ruthlessly efficient workhorse that Rust uses to make your programs blisteringly fast without you ever having to think about calling free.