Stack and Heap
Memory used in programs are allocated in two distinct parts of the available memory space:
Stack
- Used for local variables
- Values have fixed size
- Allocation and deallocation is fast (by moving the stack pointer)
Heap
- Size of values are determined at runtime
- Allocation and deallocation is slower (book-keeping needed to manage the contents of the heap)
Stack and Heap: Stack
#include <stdint.h>
int main(void) {
uint32_t x = 5;
}
fn main() {
let x: u32 = 5;
}
Stack and Heap: Heap
#include <malloc.h>
#include <stdint.h>
int main(void) {
uint32_t *x = malloc(sizeof(*x));
*x = 5;
free(x);
}
fn main() {
let x: Box<u32> = Box::new(5);
}
Dynamic Memory Management
- No garbage collector/runtime system
- Full control over memory management
-
Manual memory management through
malloc
andfree
#include <malloc.h>
#include <stdint.h>
int main(void) {
uint32_t *x = malloc(sizeof(*x));
*x = 5;
free(x);
}
- No garbage collector/runtime system
- Full control over memory management
- Automatic memory management
fn main() {
let x: Box<u32> = Box::new(5);
} // x is automatically freed here
This is done with Rust’s ownership model.
Automatic Memory Management In Rust
- A value is a sequence of bits in memory.
- A variable is a placeholder for a value, and is a component of a stack frame.
- A stack frame is a mapping of variables to values on the stack.
fn main() {
let a = 5; // <---- L1
let b = add_one(a);
}
fn add_one(x: i32) -> i32 {
let c = x + 1;
c
}
Automatic Memory Management In Rust
- A value is a sequence of bits in memory.
- A variable is a placeholder for a value, and is a component of a stack frame.
- A stack frame is a mapping of variables to values on the stack.
fn main() {
let a = 5;
let b = add_one(a);
}
fn add_one(x: i32) -> i32 {
let c = x + 1; // <---- L2
c
}
Automatic Memory Management In Rust
- A value is a sequence of bits in memory.
- A variable is a placeholder for a value, and is a component of a stack frame.
- A stack frame is a mapping of variables to values on the stack.
fn main() {
let a = 5;
let b = add_one(a); // <---- L3
}
fn add_one(x: i32) -> i32 {
let c = x + 1;
c
}
Ownership Rules
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Ownership Rules
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
fn main() {
let a = 5;
{
let b = 6;
{
let c = 7; // <--- L1
}
println!("{b}");
}
println!("{a}");
}
Ownership Rules
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
fn main() {
let a = 5;
{
let b = 6;
{
let c = 7;
}
println!("{b}"); // <--- L2
}
println!("{a}");
}
Ownership Rules
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
fn main() {
let a = 5;
{
let b = 6;
{
let c = 7;
}
println!("{b}");
}
println!("{a}"); // <--- L3
}
Ownership Rules (2)
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
fn main() {
let a = 5;
{
let b = Box::new(6);
println!("{b}"); // <--- L1
}
println!("{a}");
}
Ownership Rules (2)
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
fn main() {
let a = 5;
{
let b = Box::new(6);
println!("{b}");
}
println!("{a}"); // <--- L2
}
Ownership Rules (3)
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
- Assignment moves ownership of the value.
- This invalidates the original variable.
fn main() {
let a = Box::new(5); // <--- L1
let b = a;
}
Ownership Rules (3)
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
- Assignment moves ownership of the value.
- This invalidates the original variable.
fn main() {
let a = Box::new(5);
let b = a; // <--- L2
}
Ownership Moved (1)
- Assignment moves ownership.
fn main() {
let s1 = String::from("Hello world!");
let s2 = s1;
println!("{} {}", s1, s2);
}
Ownership Moved (2)
- Cloning avoids moving ownership.
fn main() {
let s1 = String::from("Hello world!");
let s2 = s1.clone();
println!("{} {}", s1, s2);
}
-
Types that implement the
Clone
trait are clonable.
Ownership Moved (3)
-
Structs can automatically implement the
Clone
trait throughderive
if the contents of the struct are alsoClone
.
#[derive(Debug, Clone)]
struct User {
active: bool,
username: String,
sign_in_count: u64,
}
fn main() {
let user_1 = User {
active: true, username: String::from("crab"), sign_in_count: 0
};
let user_2 = user_1.clone();
println!("{user_1:?} and {user_2:?}");
}
Q/A
Q: Why are structs not Clone
by default?
A: Flexibility. Clone
can be manually implemented to provide different functionality (eg. for reference counting, see std::sync::Rc
)
Q: Why do assignments not .clone()
by default?
A: Performance. Cloning may be expensive, so we want explicit intention.
Q: Is using .clone()
a lot bad practice?
A: Possibly, however always measure.
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.
Q: For types like integers, having to always .clone()
seems like a big hassle… Is there a better way?
A: There is!
Copy
- By default, variable assignment has move semantics.
-
However, if a type implements the
Copy
trait, it has copy semantics. -
Types that are
Copy
are implicitly duplicated on assignment.
fn main() {
let a = 5; // <---- L1
let mut b = a;
b += 1;
}
Copy
- By default, variable assignment has move semantics.
-
However, if a type implements the
Copy
trait, it has copy semantics. -
Types that are
Copy
are implicitly duplicated on assignment.
fn main() {
let a = 5;
let mut b = a; // <---- L2
b += 1;
}
Copy
- By default, variable assignment has move semantics.
-
However, if a type implements the
Copy
trait, it has copy semantics. -
Types that are
Copy
are implicitly duplicated on assignment.
fn main() {
let a = 5;
let mut b = a;
b += 1; // <---- L3
}
Copy (2)
-
Structs can automatically implement the
Copy
trait throughderive
if the contents of the struct are alsoCopy
. -
Any struct that is
Copy
(”cheap to duplicate”) must also beClone
(”maybe expensive to duplicate”).
#[derive(Debug, Clone, Copy)]
struct AnonymousUser {
active: bool,
sign_in_count: u64,
}
fn main() {
let user_1 = AnonymousUser {
active: true, sign_in_count: 0
};
let user_2 = user_1;
println!("{user_1:?} and {user_2:?}");
}
Q/A
Q: You mentioned explicit intention earlier, why are Copy
types implicitly duplicated on assignment?
A: Ergonomics, having to .clone()
everywhere creates clutter.
Q: Can structs with non-Copy
fields implement Copy
?
A: No. Copy
indicates that the type can be duplicated with a simple memcpy
, and complicated implementations for more functionality are not allowed.
Q: Why do structs not default to being Copy
if all of its fields are Copy
?
A: Explicit intention. Accidentally adding a non-Copy
field to a struct may break code that relies on the struct implicitly being Copy
.
Q: ?
Ownership and Functions
Passing a value to a function is similar to performing variable assignment.
fn main() {
let s = String::from("crab");
takes_ownership(s);
// `s` is no longer valid here
let x = 5;
makes_copy(x);
// `x` can still be used here
}
fn takes_ownership(some_string: String) {
println!("{some_string}");
} // `s` is dropped here and memory is freed
fn makes_copy(some_integer: i32)
println!("{some_integer}");
}
Ownership and Functions (2)
Q: When is the memory for "crab"
freed?
fn main() {
let s1 = returns_ownership();
let s2 = takes_and_returns(s1);
}
fn returns_ownership() -> String {
String::from("crab")
}
fn takes_and_returns(some_string: String) -> String {
some_string
}