Understanding Rust Memory Management

January 11, 2024 (1y ago)

rust

Stack & Heap

Ownership

fn main() {
    let s1 = String::from("hello");  // `s1` owns the heap-allocated string
    let s2 = s1;  // Ownership is transferred to `s2`, s1 no longer has the right to manage/read the data in the heap
    // println!("{}", s1);  // This line would cause a compile-time error
    println!("{}", s2);  // This works
} // `s2` goes out of scope here, and the memory is freed

Notes: In some cases, if the variable that's being re-assigned to a new variable has Types with the Copy Trait, then the value will just be copied example. let age = 23; // this has i32 type let age2 = age; // this will just copy the age value and not transfer the ownership printlin!("{}, {}", age, age2) // both age and age2 will work here

Borrowing

Types of Borrowing

  1. Immutable Borrowing: When you borrow a variable immutably, you're asking for permission to read the variable but not modify it. You can have multiple immutable borrows at the same time, as long as there are no active mutable borrows.
  2. Mutable Borrowing: When you borrow a variable mutably, you're asking for permission to modify the variable. There can only be one mutable borrow at a time, and while it's active, no other mutable or immutable borrows can be made.
  3. Scope and Lifetime: The scope of the borrow is limited, meaning the borrowed variable must not outlive the scope of its owner. This is ensured by Rust's borrow checker at compile time, which helps prevent issues like dangling pointers or data races.
  4. Safety: Through borrowing, Rust provides a way to safely and efficiently share data between different parts of a program without the pitfalls of manual memory management found in some other languages.

Why Borrow?

Memory Allocation Optimization

  1. Avoiding Unnecessary Copies: Borrowing allows functions to use data without needing to create a copy of it. This is especially important for large data structures. Without borrowing, every function call or assignment involving these structures would require copying all their contents, which is inefficient in terms of both memory usage and performance.
  2. Efficient Memory Usage: By using references to data, Rust programs can be more memory efficient. Borrowing allows multiple parts of your program to read (and in the case of mutable references, modify) the same piece of data without duplicating it in memory.

Safety

  1. Preventing Data Races: In a concurrent context, borrowing rules ensure that data cannot be simultaneously modified from one place and read or modified from another, which could otherwise lead to data races. Rust's compile-time checks enforce that either a single mutable reference or any number of immutable references can exist for a particular piece of data.
  2. Avoiding Dangling Pointers: Rust's borrowing system ensures that references always point to valid data. The borrow checker at compile time ensures that a reference cannot outlive the data it points to, preventing dangling pointers.
  3. Compile-time Checks: The borrowing rules are enforced at compile time, which means many potential errors (like use-after-free, double free, and data races) are caught before the program even runs.

Slicing

let mut vec = vec![1, 2, 3, 4, 5];
let slice = &mut vec[1..4]; // A mutable slice referencing elements 2, 3, and 4 of vec
slice[0] = 10;

Note: range syntax is not index..index it's n_element(..sliced)n_element, where n_elements are excluded

That's it for now, I'll try to post more about my journey on learning rust. Cheers!