Understanding Memory Management in Rust
Stack, Heap, and Object Creation
Table of Contents
Introduction
Before diving in, you may want to refer to my earlier post on Rust for a more foundational overview.
Rust’s memory model represents a significant departure from garbage-collected languages like Java, Python, or JavaScript. While these languages abstract away memory management, Rust provides fine-grained control without sacrificing safety. Understanding how Rust manages memory is fundamental to writing efficient and correct programs.
The Fundamental Question
Consider this simple declaration:
let s = String::from("hello");
This raises several important questions:
- Where does this data reside? Stack? Heap? Both?
- How does the compiler determine allocation strategy?
- What is the memory layout of this structure?
Stack vs Heap: Core Concepts
Computer memory can be conceptualized as two distinct storage systems with different characteristics.
The Stack
The Stack operates as a LIFO (Last In, First Out) data structure. It provides fast, predictable memory allocation with automatic deallocation when variables go out of scope.
Stack Characteristics:
- Extremely fast allocation and deallocation (pointer manipulation)
- Automatic memory management through scope-based cleanup
- Fixed-size data only
- Sequential memory layout
The Heap
The Heap provides dynamic memory allocation for data with unknown or variable sizes. While slower than stack allocation, it offers flexibility for growable data structures and long-lived allocations.
Heap Characteristics:
- Slower allocation (requires finding suitable memory blocks)
- Manual management (though Rust automates this through ownership)
- Supports dynamic-size data
- Data can outlive function scope
Allocation Examples
// Stack allocation: Fixed size, compile-time known
let x: i32 = 42; // 4 bytes
let y: bool = true; // 1 byte
let arr: [i32; 5] = [1, 2, 3, 4, 5]; // 20 bytes
// Heap allocation: Dynamic size
let s = String::from("hello"); // Growable string
let v = vec![1, 2, 3]; // Growable vector
Performance Comparison
| Feature | Stack | Heap |
|---|---|---|
| Speed | Fast – Just move a pointer | Slower – Search for space |
| Allocation Cost | ~1 CPU cycle | ~100+ CPU cycles |
| Deallocation | Automatic (pointer adjustment) | Must be explicitly freed |
| Size | Must be known at compile time | Can be dynamic/unknown |
| Lifetime | Tied to function scope | Can live beyond function |
| Cache Locality | Excellent (contiguous) | Poor (scattered) |
String Internals: A Detailed Analysis
The String type exemplifies Rust’s hybrid approach to memory management:
let s = String::from("hello");
This single line creates a structure that spans both stack and heap.
Memory Layout
┌─────────── STACK ───────────┐
│ Variable: s │
│ ptr: 0x1234 ────────┐ │ (pointer to heap)
│ capacity: 5 │ │ (allocated space)
│ length: 5 │ │ (used space)
└────────────────────────────┘ │
│
┌─────────── HEAP ────────────┐ │
│ Address 0x1234: │ │
│ [ h | e | l | l | o ] ◄───┘
└─────────────────────────────┘
Stack Component (24 bytes on 64-bit systems):
- Pointer to heap allocation (8 bytes)
- Capacity field (8 bytes)
- Length field (8 bytes)
Heap Component (5 bytes):
- UTF-8 encoded character data
Design Rationale
This design optimizes for common operations. When assigning or passing strings:
let s1 = String::from("hello");
let s2 = s1; // Copies only 24 bytes (stack metadata)
The heap data remains in place; only the stack metadata is copied. However, Rust’s ownership system ensures memory safety by invalidating s1 after the move:
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // ❌ Compile error: value borrowed after move
Compiler-Driven Allocation Strategy
The Rust compiler determines allocation strategy through type analysis.
Type-Based Allocation
The compiler examines type definitions to determine memory layout. Consider the String type (simplified):
pub struct String {
vec: Vec<u8>,
}
pub struct Vec<T> {
ptr: *mut T, // Raw pointer indicates heap allocation
cap: usize, // Capacity
len: usize, // Length
}
The presence of a raw pointer (*mut T) signals to the compiler that this type manages heap memory. The compiler then:
- Allocates the struct’s metadata on the stack
- Generates code for heap allocation of the pointed-to data
- Implements
Droptrait for automatic cleanup
Contrast this with a simple struct:
struct Point {
x: i32,
y: i32,
}
No pointers, no indirection. The compiler places this entirely on the stack (8 bytes total).
Automatic Type Analysis
The compiler performs this analysis automatically:
let p = Point { x: 10, y: 20 }; // Compiler: 8 bytes, stack
let s = String::from("hi"); // Compiler: 24 bytes stack + heap
let v = vec![1, 2, 3]; // Compiler: 24 bytes stack + heap
Object Creation Patterns in Rust
Rust doesn’t have classes in the traditional object-oriented sense. Instead, it uses structs with associated implementations.
Type Definition
struct User {
name: String,
age: i32,
email: String,
}
Pattern 1: Direct Instantiation
let user = User {
name: String::from("Alice"),
age: 30,
email: String::from("[email protected]"),
};
Memory Layout:
┌─────────── STACK ───────────┐
│ user: │
│ name: (String, 24 bytes) │ ──➤ heap: "Alice"
│ age: 30 (4 bytes) │
│ email: (String, 24 bytes) │ ──➤ heap: "[email protected]"
└─────────────────────────────┘
Pattern 2: Constructor Functions
impl User {
// Associated function (static method)
fn new(name: String, age: i32, email: String) -> Self {
Self { name, age, email }
}
// Factory with defaults
fn default() -> Self {
Self {
name: String::from("Unknown"),
age: 0,
email: String::from("no-email"),
}
}
// Factory with validation
fn create(name: String, age: i32, email: String) -> Result<Self, String> {
if age < 0 {
return Err("Age cannot be negative".to_string());
}
Ok(Self { name, age, email })
}
}
// Usage
let user1 = User::new(
String::from("Alice"),
30,
String::from("[email protected]")
);
let user2 = User::default();
let user3 = User::create(
String::from("Bob"),
-5,
String::from("[email protected]")
)?; // Returns Err
Pattern 3: Builder Pattern
For complex types with many optional parameters, the builder pattern provides ergonomic construction:
struct UserBuilder {
name: Option<String>,
age: Option<i32>,
email: Option<String>,
}
impl UserBuilder {
fn new() -> Self {
Self {
name: None,
age: None,
email: None,
}
}
fn name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
fn age(mut self, age: i32) -> Self {
self.age = Some(age);
self
}
fn email(mut self, email: String) -> Self {
self.email = Some(email);
self
}
fn build(self) -> Result<User, String> {
Ok(User {
name: self.name.ok_or("Name is required")?,
age: self.age.ok_or("Age is required")?,
email: self.email.ok_or("Email is required")?,
})
}
}
// Usage
let user = UserBuilder::new()
.name(String::from("Alice"))
.age(30)
.email(String::from("[email protected]"))
.build()?;
Explicit Heap Allocation with Box
The Box<T> smart pointer provides explicit heap allocation:
let user = Box::new(User {
name: String::from("Alice"),
age: 30,
email: String::from("[email protected]"),
});
Memory Layout:
┌─────────── STACK ───────────┐
│ user: Box (8 bytes) │
│ ptr: 0x5678 ──────────────┼─┐
└─────────────────────────────┘ │
│
┌─────────── HEAP ────────────┐ │
│ Address 0x5678: │ │
│ User { │◄┘
│ name: (String) ────────┼──➤ heap: "Alice"
│ age: 30 │
│ email: (String) ────────┼──➤ heap: "[email protected]"
│ } │
└─────────────────────────────┘
Use Cases for Box
- Large types: Moving large structs on the stack is expensive; Box enables cheap pointer moves
- Recursive types: Types that contain themselves must use indirection
- Trait objects: Dynamic dispatch requires heap allocation
Automatic Memory Management Through RAII
Rust implements automatic cleanup through RAII (Resource Acquisition Is Initialization), also known as scope-based resource management:
{
let s = String::from("hello");
let v = vec![1, 2, 3];
// ... operations ...
} // ← Compiler inserts Drop calls here
// 1. Deallocates heap memory for s
// 2. Deallocates heap memory for v
// 3. No garbage collector needed
When variables go out of scope, Rust automatically calls their Drop implementation, which handles cleanup. This provides:
- Deterministic cleanup: Resources freed immediately at scope exit
- Zero overhead: No garbage collector runtime
- Guaranteed cleanup: Impossible to forget to free resources
The Ownership System
Rust’s ownership system is the cornerstone of its memory safety guarantees. It enforces three fundamental rules at compile time, eliminating entire classes of bugs without runtime overhead.
The Three Rules of Ownership
- Each value in Rust has a single owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped
Ownership Transfer (Move Semantics)
When assigning or passing values, ownership transfers to the new location:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership moves from s1 to s2
// println!("{}", s1); // ❌ Compile error: s1 no longer valid
println!("{}", s2); // ✓ Works: s2 owns the string
}
Real-Time Example: File Handle Ownership
Consider opening a file. The file handle must have exactly one owner to prevent double-close errors:
use std::fs::File;
use std::io::{Write, Result};
fn write_log(mut file: File) -> Result<()> {
file.write_all(b"Log entry")?;
Ok(())
} // File automatically closed when it goes out of scope
fn main() -> Result<()> {
let log_file = File::create("app.log")?;
write_log(log_file); // Ownership transferred to function
// log_file.write_all(b"more"); // ❌ Error: log_file moved
Ok(())
}
Copy vs Move Types
Types that implement the Copy trait are duplicated rather than moved:
// Integers implement Copy - they're duplicated
let x = 5;
let y = x; // x is copied, both x and y are valid
println!("x = {}, y = {}", x, y); // ✓ Both work
// String doesn't implement Copy - it's moved
let s1 = String::from("hello");
let s2 = s1; // s1 is moved, only s2 is valid
Clone: Explicit Deep Copy
When deep copying is needed, use the clone() method:
let s1 = String::from("hello");
let s2 = s1.clone(); // Explicit deep copy
println!("s1 = {}, s2 = {}", s1, s2); // ✓ Both valid
Borrowing and References
Borrowing allows code to reference a value without taking ownership. Rust enforces borrowing rules at compile time to prevent data races and dangling references.
Immutable Borrowing
Multiple immutable references can exist simultaneously:
fn calculate_length(s: &String) -> usize {
s.len() // Can read, but not modify
}
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // Borrow s
println!("Length of '{}' is {}", s, len); // s still valid
}
Mutable Borrowing
Only one mutable reference can exist at a time, preventing data races:
fn append_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut s = String::from("hello");
append_world(&mut s); // Mutable borrow
println!("{}", s); // Prints: hello, world
}
The Borrowing Rules
- At any given time, you can have either one mutable reference or any number of immutable references
- References must always be valid (no dangling references)
Real-Time Example: Thread-Safe Configuration
Borrowing rules prevent common concurrency bugs:
struct Config {
max_connections: u32,
timeout: u64,
}
fn read_config(config: &Config) -> u32 {
config.max_connections
}
fn update_config(config: &mut Config, new_max: u32) {
config.max_connections = new_max;
}
fn main() {
let mut config = Config {
max_connections: 100,
timeout: 30,
};
// Multiple readers - safe
let conn1 = read_config(&config);
let conn2 = read_config(&config);
// Single writer - safe
update_config(&mut config, 200);
// ❌ This would fail to compile:
// let ref1 = &config;
// let ref2 = &mut config; // Error: cannot borrow as mutable
}
Slices: Borrowing Parts of Collections
Slices let you reference a contiguous sequence without owning it:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i]; // Slice from start to space
}
}
&s[..] // Return entire string
}
fn main() {
let s = String::from("hello world");
let word = first_word(&s);
println!("First word: {}", word); // Prints: hello
}
Lifetimes: Ensuring Reference Validity
Lifetimes are Rust’s mechanism for ensuring that references remain valid. They prevent dangling references at compile time.
The Dangling Reference Problem
In languages without lifetime checking, this code would compile but crash:
// ❌ This doesn't compile in Rust
fn dangling_reference() -> &String {
let s = String::from("hello");
&s // Error: s dropped at end of function
}
Lifetime Annotations
When the compiler cannot infer lifetimes, explicit annotations are required:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string");
let string2 = String::from("short");
let result = longest(&string1, &string2);
println!("Longest: {}", result);
}
'a indicates that the returned reference will be valid as long as both input references are valid.
Real-Time Example: Cache with References
Lifetimes enable safe caching of borrowed data:
struct Context<'a> {
user_agent: &'a str,
ip_address: &'a str,
}
impl<'a> Context<'a> {
fn new(user_agent: &'a str, ip_address: &'a str) -> Self {
Context { user_agent, ip_address }
}
fn log_info(&self) {
println!("User-Agent: {}, IP: {}", self.user_agent, self.ip_address);
}
}
fn main() {
let ua = String::from("Mozilla/5.0");
let ip = String::from("192.168.1.1");
let ctx = Context::new(&ua, &ip);
ctx.log_info();
} // ua and ip must outlive ctx
Context cannot outlive the data it references, preventing use-after-free.
Static Lifetime
The 'static lifetime indicates data that lives for the entire program duration:
let s: &'static str = "String literals have static lifetime";
const VERSION: &str = "1.0.0"; // Implicitly 'static
Concurrency and Multi-threading
Rust’s ownership and type system enable fearless concurrency – preventing data races at compile time without runtime overhead.
Thread Safety Basics
Rust uses the Send and Sync marker traits to ensure thread safety:
- Send: Type can be transferred between threads
- Sync: Type can be shared between threads (safe for concurrent access)
Creating Threads
Spawning threads with ownership transfer:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("Thread: {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("Main: {}", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap(); // Wait for thread to finish
}
Message Passing with Channels
Channels provide safe communication between threads:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let messages = vec![
String::from("hello"),
String::from("from"),
String::from("thread"),
];
for msg in messages {
tx.send(msg).unwrap();
}
});
for received in rx {
println!("Received: {}", received);
}
}
Shared State with Mutex
Mutex provides mutual exclusion for shared mutable state:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap()); // Prints: 10
}
- Arc: Atomic Reference Counted pointer for thread-safe shared ownership
- Mutex: Ensures only one thread can access data at a time
Real-World Example: Concurrent Web Scraper
Parallel processing with thread pool:
use std::sync::{Arc, Mutex};
use std::thread;
struct ScraperStats {
pages_processed: usize,
errors: usize,
}
fn scrape_url(url: &str) -> Result<String, String> {
// Simulate scraping
if url.contains("error") {
Err(String::from("Failed to fetch"))
} else {
Ok(format!("Content from {}", url))
}
}
fn main() {
let urls = vec![
"https://example.com/1",
"https://example.com/2",
"https://error.com/3",
"https://example.com/4",
];
let stats = Arc::new(Mutex::new(ScraperStats {
pages_processed: 0,
errors: 0,
}));
let mut handles = vec![];
for url in urls {
let stats = Arc::clone(&stats);
let handle = thread::spawn(move || {
match scrape_url(url) {
Ok(content) => {
println!("✓ {}", content);
let mut s = stats.lock().unwrap();
s.pages_processed += 1;
}
Err(e) => {
println!("✗ Error for {}: {}", url, e);
let mut s = stats.lock().unwrap();
s.errors += 1;
}
}
});
handles.push(handle);
}
// Wait for all threads
for handle in handles {
handle.join().unwrap();
}
let final_stats = stats.lock().unwrap();
println!("\nProcessed: {}, Errors: {}",
final_stats.pages_processed,
final_stats.errors
);
}
Read-Write Lock for Concurrent Reads
RwLock allows multiple readers or one writer:
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// Multiple reader threads
for i in 0..5 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
let r = data.read().unwrap();
println!("Reader {}: {:?}", i, *r);
});
handles.push(handle);
}
// One writer thread
let data_clone = Arc::clone(&data);
let writer = thread::spawn(move || {
let mut w = data_clone.write().unwrap();
w.push(4);
println!("Writer: added element");
});
handles.push(writer);
for handle in handles {
handle.join().unwrap();
}
}
Common Memory Patterns
Pattern 1: Stack-Only Struct
struct Point {
x: i32,
y: i32,
}
let p = Point { x: 10, y: 20 }; // 8 bytes, all stack
Pattern 2: Struct with Heap Fields
struct Person {
name: String, // Heap-allocated string data
age: i32, // Stack value
}
let person = Person {
name: String::from("Alice"),
age: 30,
};
Pattern 3: Boxed Struct
let person = Box::new(Person {
name: String::from("Alice"),
age: 30,
});
Pattern 4: Reference Counted (Single-threaded)
use std::rc::Rc;
let data = Rc::new(vec![1, 2, 3]);
let data2 = Rc::clone(&data);
let data3 = Rc::clone(&data);
// Shared ownership: deallocated when last Rc drops
Pattern 5: Atomic Reference Counted (Multi-threaded)
use std::sync::Arc;
let data = Arc::new(vec![1, 2, 3]);
// Safe to clone and send across threads
Practical Example: Cache Implementation
A simple cache demonstrates these memory concepts in practice:
use std::collections::HashMap;
struct Cache {
data: HashMap<String, String>,
max_size: usize,
}
impl Cache {
fn new(max_size: usize) -> Self {
Self {
data: HashMap::new(),
max_size,
}
}
fn put(&mut self, key: String, value: String) {
if self.data.len() >= self.max_size {
// Simple eviction: remove arbitrary entry
if let Some(k) = self.data.keys().next().cloned() {
self.data.remove(&k);
}
}
self.data.insert(key, value);
}
fn get(&self, key: &str) -> Option<&String> {
self.data.get(key)
}
}
// Usage
fn main() {
let mut cache = Cache::new(100);
cache.put("user:1".to_string(), "Alice".to_string());
if let Some(name) = cache.get("user:1") {
println!("Found: {}", name);
}
}
Memory Analysis:
Cachestruct: Stack (~24 bytes)- HashMap internal structure: Heap
- Each key String: Heap-allocated characters
- Each value String: Heap-allocated characters
- Automatic cleanup: All heap memory freed when
cachegoes out of scope
Real-World Example: HTTP Request Pool
This comprehensive example demonstrates ownership, borrowing, lifetimes, and concurrency in a production-style HTTP request pooling system.
Architecture Overview
The request pool manages concurrent HTTP requests with connection reuse, timeout handling, and thread-safe statistics tracking.
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::{Duration, Instant};
// Connection with lifetime bound to host string
struct Connection<'a> {
host: &'a str,
created_at: Instant,
request_count: usize,
}
impl<'a> Connection<'a> {
fn new(host: &'a str) -> Self {
Connection {
host,
created_at: Instant::now(),
request_count: 0,
}
}
fn is_stale(&self, max_age: Duration) -> bool {
self.created_at.elapsed() > max_age
}
fn send_request(&mut self, path: &str) -> String {
self.request_count += 1;
format!("Response from {}{} (req #{})",
self.host, path, self.request_count)
}
}
// Thread-safe statistics
struct PoolStats {
total_requests: usize,
active_connections: usize,
cache_hits: usize,
}
impl PoolStats {
fn new() -> Self {
PoolStats {
total_requests: 0,
active_connections: 0,
cache_hits: 0,
}
}
}
// Connection pool with shared ownership
struct RequestPool {
// RwLock: multiple readers, single writer
connections: Arc<RwLock<HashMap<String, Vec<Connection<'static>>>>>,
// Mutex: exclusive access for modifications
stats: Arc<Mutex<PoolStats>>,
max_conn_age: Duration,
}
impl RequestPool {
fn new() -> Self {
RequestPool {
connections: Arc::new(RwLock::new(HashMap::new())),
stats: Arc::new(Mutex::new(PoolStats::new())),
max_conn_age: Duration::from_secs(30),
}
}
// Shared reference - multiple threads can call
fn get_connection(&self, host: String) -> Option<Connection<'static>> {
let mut connections = self.connections.write().unwrap();
if let Some(pool) = connections.get_mut(&host) {
// Remove stale connections
pool.retain(|conn| !conn.is_stale(self.max_conn_age));
if let Some(conn) = pool.pop() {
let mut stats = self.stats.lock().unwrap();
stats.cache_hits += 1;
return Some(conn);
}
}
None
}
fn return_connection(&self, conn: Connection<'static>) {
let mut connections = self.connections.write().unwrap();
let host = conn.host.to_string();
connections
.entry(host)
.or_insert_with(Vec::new)
.push(conn);
}
fn execute_request(&self, host: String, path: &str) -> String {
// Try to reuse connection
let mut conn = self
.get_connection(host.clone())
.unwrap_or_else(|| {
// Create new connection (requires 'static lifetime)
let static_host: &'static str = Box::leak(host.clone().into_boxed_str());
Connection::new(static_host)
});
// Update stats
{
let mut stats = self.stats.lock().unwrap();
stats.total_requests += 1;
}
// Send request
let response = conn.send_request(path);
// Return connection to pool
self.return_connection(conn);
response
}
fn print_stats(&self) {
let stats = self.stats.lock().unwrap();
let connections = self.connections.read().unwrap();
let total_pooled: usize = connections
.values()
.map(|pool| pool.len())
.sum();
println!("\n📊 Pool Statistics:");
println!(" Total Requests: {}", stats.total_requests);
println!(" Cache Hits: {}", stats.cache_hits);
println!(" Pooled Connections: {}", total_pooled);
println!(" Hit Rate: {:.1}%",
(stats.cache_hits as f64 / stats.total_requests as f64) * 100.0
);
}
}
Using the Pool Concurrently
Demonstrate concurrent access with multiple threads:
fn main() {
let pool = Arc::new(RequestPool::new());
let mut handles = vec![];
// Simulate concurrent requests from multiple threads
for thread_id in 0..5 {
let pool = Arc::clone(&pool);
let handle = thread::spawn(move || {
// Each thread makes requests to different hosts
let hosts = vec![
String::from("api.example.com"),
String::from("cdn.example.com"),
];
for i in 0..3 {
let host = &hosts[i % hosts.len()];
let path = format!("/data/{}", i);
let response = pool.execute_request(
host.clone(),
&path
);
println!("[Thread {}] {}", thread_id, response);
// Simulate processing time
thread::sleep(Duration::from_millis(10));
}
});
handles.push(handle);
}
// Wait for all threads to complete
for handle in handles {
handle.join().unwrap();
}
// Print final statistics
pool.print_stats();
}
Key Concepts Demonstrated
1. Ownership
Connectionownership transferred when returned to pool- Each host string has clear ownership via
'staticlifetime
2. Borrowing
&selfallows concurrent reads viaRwLock- Mutable borrows protected by
Mutexfor exclusive access
3. Lifetimes
Connection<'a>ensures host string outlives connection'staticlifetime for pooled connections (must live entire program)
4. Concurrency
Arcenables shared ownership across threadsRwLockallows multiple concurrent readersMutexensures exclusive access for statistics updates
Memory Layout Analysis
Stack Allocations:
Arcpointer: 8 bytesDuration: 16 bytesInstant: 16 bytes
Heap Allocations:
HashMap: Dynamic, grows as neededVec<Connection>: One per host- Host strings: Leaked to obtain
'staticlifetime RwLockandMutexdata
Performance Characteristics
- Connection Reuse: Eliminates expensive setup costs
- Lock Granularity:
RwLockallows concurrent reads,Mutexfor writes - Stale Detection: Automatic removal of expired connections
- Zero-Copy: Connections moved, not cloned
Memory Layout Reference
| Type | Location | Size | Example |
|---|---|---|---|
| i32, f64, bool, char | Stack | Fixed (4-8 bytes) | let x: i32 = 42; |
| [T; N] (array) | Stack | Fixed (N × sizeof(T)) | let a = [1, 2, 3]; |
| String | Stack + Heap | Dynamic | let s = String::from("hi"); |
| Vec<T> | Stack + Heap | Dynamic | let v = vec![1, 2, 3]; |
| Box<T> | Stack + Heap | Fixed (ptr size) | let b = Box::new(42); |
| &T (reference) | Stack | Fixed (ptr size) | let r = &x; |
| Rc<T> / Arc<T> | Stack + Heap | Fixed (ptr size) | let a = Arc::new(data); |
Decision Heuristics
Choose Stack When:
- Size is known at compile time
- Data is small (typically < 1KB)
- Data doesn’t need to outlive the current scope
- Maximum performance is required
Choose Heap When:
- Size is unknown or varies at runtime
- Data must outlive the current scope
- Data is large and copying would be expensive
- Shared ownership is needed
Key Principles
- Stack allocation: Fast, automatic, fixed-size data
- Heap allocation: Flexible, dynamic-size, requires ownership management
- Compiler-driven: Allocation strategy determined by type analysis
- Zero-cost abstraction: No runtime overhead for memory safety
- RAII: Automatic cleanup through scope-based resource management
- Ownership system: Compile-time prevention of memory errors
Further Reading
- The Rust Programming Language – Ownership
- The Rust Programming Language – Smart Pointers
- The Rustonomicon – Advanced Memory Topics
Understanding Rust’s memory model provides the foundation for writing efficient, safe systems software. The combination of compile-time safety checks and zero-cost abstractions enables performance comparable to C/C++ while eliminating entire classes of memory bugs.