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.
    • Strings, Vecs, and other growable data structures.
    • Objects created with Rc<T> or Arc<T> for shared ownership.
  • 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.
  • 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 by Box).
    • 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.