Rust: Heap & Stack
2024-08-15
In Rust, the heap and the stack are two distinct regions of memory used for data storage, each with its own characteristics and trade-offs.
Stack
- Purpose: The stack is primarily used for storing data with a known, fixed size at compile time. This includes:
- Primitive types (integers, floats, booleans)
- Structs and enums with fixed-size fields
- References to data stored elsewhere
- Allocation:
- Fast and efficient: Allocation on the stack is extremely fast as it simply involves moving the stack pointer.
- Automatic deallocation: When a function returns, the data on the stack associated with that function is automatically deallocated.
- Limitations:
- Limited size: The stack typically has a fixed size (determined at compile-time or by the operating system).
- No dynamic resizing: Data on the stack cannot grow or shrink in size after being allocated.
Example of using the stack
fn main() {
let x: i32 = 5; // 'x' is allocated on the stack
let y: f64 = 3.14; // 'y' is also allocated on the stack
let point = Point { x: 10, y: 20 }; // 'point' is allocated on the stack because its size is known at compile time.
} // 'x', 'y', and 'point' are automatically deallocated when 'main' returns
Heap
- Purpose: The heap is used for storing data whose size is unknown at compile time or can change during the program’s execution.
- Data allocated with the
Box<T>
smart pointer. String
s,Vec
s, and other growable data structures.- Objects created with
Rc<T>
orArc<T>
for shared ownership.
- Data allocated with the
- Allocation:
- Explicit allocation: You need to explicitly allocate memory on the heap using functions like
Box::new()
. - Manual deallocation (or smart pointers):
- You are responsible for deallocating heap-allocated memory when it’s no longer needed.
- Rust’s smart pointers (like
Box
,Rc
,Arc
) handle this deallocation automatically when they go out of scope.
- Explicit allocation: You need to explicitly allocate memory on the heap using functions like
- Advantages:
- Flexible size: Data on the heap can grow or shrink as needed during the program’s execution.
- Global access: Data on the heap can be accessed from anywhere in your program as long as you have a valid reference to it.
Example of using the heap
fn main() {
let s = String::from("hello"); // 's' is allocated on the heap
let v = vec![1, 2, 3]; // 'v' is also allocated on the heap
let b = Box::new(10); // Explicitly allocate an integer on the heap
} // Rust's ownership system ensures 's', 'v', and the memory pointed to by 'b' are automatically deallocated when they go out of scope
Key Differences and When to Use Each
Feature | Stack | Heap |
---|---|---|
Allocation speed | Very fast | Slower (requires searching for a suitable memory block) |
Deallocation | Automatic | Manual (or using smart pointers) |
Size | Fixed at compile time | Can grow or shrink dynamically |
Access | Local to the function | Global (via references or pointers) |
Use cases | Primitives, structs, enums, references | Dynamically sized data, shared ownership, data that needs to outlive its current scope |
Memory Allocation
+-------------------+
| | High memory addresses
| Heap |
| |
| (Dynamically |
| allocated) |
| |
| |
| |
+-------------------+
| |
| Stack |
| |
| (Fixed-size, |
| function-local) |
| |
| |
+-------------------+
| | Low memory addresses
Key Points:
Stack:
- Grows downwards from higher memory addresses.
- Stores data with known sizes at compile-time (e.g., primitive types, fixed-size structs, references).
- Allocations and deallocations are very fast.
- Data is automatically deallocated when the function it belongs to returns.
Heap:
- Grows upwards from lower memory addresses.
- Stores data with sizes unknown at compile-time or that can change during runtime (e.g.,
String
,Vec
, data owned byBox
). - Allocations and deallocations are slower than on the stack.
- Requires explicit deallocation or the use of smart pointers (like
Box
) to manage memory safely.
Visual Example
Consider this Rust code:
fn main() {
let x: i32 = 5; // 'x' is on the stack
let s = String::from("hello"); // 's' (the String data) is on the heap,
// but the 's' variable (which holds metadata like length and capacity) is on the stack
let b = Box::new(10); // 'b' (the Box pointer) is on the stack,
// the actual integer value '10' is on the heap
}
Here’s a rough visualization of how this might look in memory:
+-------------------+
| |
| Heap |
| "hello" | (Data for 's')
| 10 | (Data for 'b')
| |
+-------------------+
| |
| Stack |
| b | (Box pointer)
| s | (String metadata)
| x: 5 |
| |
+-------------------+
Important Notes:
- This is a simplified representation. The actual memory layout can be more complex, depending on the compiler and optimization settings.
- The key takeaway is the conceptual difference between the stack and the heap and how Rust leverages them for different types of data.