Memory Without a Garbage Collector
Rust has no garbage collector and no manual free. Memory is released when its
owner goes out of scope, at a point the compiler can see - which means the cost
is predictable and there is no pause to tune.
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!(" dropping {}", self.0);
}
}
fn main() {
println!("entering main");
{
let _a = Noisy("a");
let _b = Noisy("b");
println!(" both alive");
} // dropped here, in reverse order of declaration
let moved = Noisy("moved");
take(moved); // dropped inside take, not here
println!("back in main");
let boxed = Box::new(Noisy("on the heap"));
drop(boxed); // released exactly here
println!("main is returning");
}
fn take(value: Noisy) {
println!(" take() received {}", value.0);
}
Every one of those lines is decided at compile time. There is no collector deciding when, and no finaliser running on a thread you do not control.
Stack, heap, and which one you get
fn main() {
// On the stack: a fixed size, known at compile time.
let point = (1_i64, 2_i64);
let array = [0_u8; 16];
// On the heap: a size decided at runtime, with a stack-sized handle.
let vector: Vec<u8> = vec![0; 1_000_000];
let text = String::from("owns its bytes");
let boxed: Box<i64> = Box::new(42);
// size_of tells you the HANDLE's size, not the data's.
println!("tuple: {} bytes", std::mem::size_of_val(&point));
println!("array: {} bytes", std::mem::size_of_val(&array));
println!("Vec: {} bytes of handle, {} of data",
std::mem::size_of_val(&vector), vector.len());
println!("String: {} bytes of handle, {} of data",
std::mem::size_of_val(&text), text.len());
println!("Box: {} bytes", std::mem::size_of_val(&boxed));
// A Vec is a pointer, a length and a capacity - three words.
println!("len {} capacity {}", vector.len(), vector.capacity());
}
Zero-cost abstractions, meant literally
An iterator chain compiles to the same machine code as the loop you would have written. The abstraction has no runtime representation at all.
fn main() {
let numbers: Vec<u64> = (1..=1_000_000).collect();
let by_loop = {
let mut total = 0u64;
for n in &numbers {
if n % 3 == 0 {
total += n * 2;
}
}
total
};
let by_iterator: u64 = numbers.iter().filter(|n| *n % 3 == 0).map(|n| n * 2).sum();
println!("loop {by_loop}");
println!("iterator {by_iterator}");
println!("identical: {}", by_loop == by_iterator);
println!("and in release mode they compile to the same instructions");
// Generics are monomorphised: one specialised copy per concrete type, so a
// generic function is as fast as a hand-written one. The cost is compile
// time and binary size, not speed.
println!("{} {}", double(21), double(1.5));
}
fn double<T: std::ops::Add<Output = T> + Copy>(value: T) -> T {
value + value
}
What still leaks
Rust prevents use-after-free and double-free. It does not prevent a leak: forgetting a value is safe, because nothing unsafe happens as a result.
use std::cell::RefCell;
use std::rc::Rc;
struct Node {
next: RefCell<Option<Rc<Node>>>,
}
fn main() {
// A reference cycle. Neither count reaches zero, so neither is dropped.
let a = Rc::new(Node { next: RefCell::new(None) });
let b = Rc::new(Node { next: RefCell::new(None) });
*a.next.borrow_mut() = Some(Rc::clone(&b));
*b.next.borrow_mut() = Some(Rc::clone(&a));
println!("a count {}, b count {}", Rc::strong_count(&a), Rc::strong_count(&b));
println!("dropping both handles now leaks the pair - Weak is the fix");
// And the explicit version, which is safe and occasionally what you want:
let leaked: &'static mut String = Box::leak(Box::new(String::from("never freed")));
println!("{leaked}");
// Breaking the cycle by hand, so this program does not actually leak.
*a.next.borrow_mut() = None;
*b.next.borrow_mut() = None;
println!("cycle broken: a count {}", Rc::strong_count(&a));
}
unsafe: a smaller language inside the language
unsafe does not turn off the borrow checker. It enables five things the
compiler cannot verify - dereferencing a raw pointer, calling an unsafe
function, implementing an unsafe trait, accessing a mutable static, and reading
a union field - and asks you to uphold the invariants yourself.
fn main() {
let mut value = 42;
// Creating raw pointers is safe; dereferencing them is not.
let r1 = &value as *const i32;
let r2 = &mut value as *mut i32;
unsafe {
println!("r1 points at {}", *r1);
*r2 = 99;
}
println!("value is now {value}");
// The real use: wrapping something unverifiable in a safe API. This is how
// Vec, RefCell and every channel in the standard library are built.
let mut data = vec![1, 2, 3, 4];
let (left, right) = split_at_mut(&mut data, 2);
left[0] = 10;
right[0] = 30;
println!("{data:?}");
}
// Safe to call, unsafe inside: the caller cannot misuse it, because the
// function guarantees the two halves do not overlap.
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len);
let ptr = values.as_mut_ptr();
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
The discipline: keep unsafe blocks small, document the invariant each one
relies on, and expose a safe API around them. Most applications never write one.
What to take away
- Memory is freed when its owner goes out of scope - no collector, no pauses.
Dropruns in reverse declaration order, and on the way out of a panic too.- A
VecorStringis a small handle on the stack pointing at heap data. - Iterators and generics are zero-cost: monomorphised into the code you would have written.
- Leaks are safe and therefore possible - reference cycles are the usual cause.
unsafeenables five specific operations; wrap it in a safe API and keep it small.