-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
239: Choosing Between Vec, Box, and Arrays for Performance
A few years ago, I was reviewing a PR for a colleague who was building a high-frequency telemetry aggregator. He had created a system that handled millions of small "window" buffers—each holding exactly 64 samples of sensor data. He used Vec<f64> for every single window because it felt like the "standard" way to handle a list of numbers in Rust. When we ran the benchmarks, the throughput was dismal. The culprit wasn't the logic; it was the memory layout. He was paying for a 24-byte metadata overhead (pointer, length, and capacity) for every single window, and the heap fragmentation from millions of tiny allocations was wrecking his L1 cache hits.
In Rust, we often default to Vec because it's flexible, but when you're chasing performance, that flexibility is a tax. Depending on whether your data size is known at compile time, known at runtime but fixed thereafter, or truly dynamic, you have three primary choices: arrays, boxed slices, and vectors. Choosing the wrong one doesn't just waste a few bytes; it changes how the CPU interacts with your data.
The Hidden Tax of the Vector
A Vec<T> is a powerhouse, but it's heavy. It lives on the stack as a small struct containing a pointer to the heap, the current length, and the capacity. That capacity field is the key: it allows the Vec to grow without reallocating every time you push an element. But if you know your buffer will always be 64 elements, that capacity field is just dead weight. More importantly, every Vec requires a separate heap allocation. If you have a Vec<Vec<T>>, you're dealing with "pointer chasing"—the CPU has to jump to one memory location to find the inner Vec's metadata, and then jump again to find the actual data. This is a recipe for cache misses.
Shrinking the Footprint with Boxed Slices
If you don't know the size of your data at compile time, but you know that once it's created, it will never change size, Box<[T]> is your best friend. You can easily convert a Vec into a boxed slice using into_boxed_slice().
let v = vec![1, 2, 3, 4, 5];
let boxed_slice: Box<[i32]> = v.into_boxed_slice();
What does this actually do? It reallocates the memory to fit the data exactly and drops the capacity field. Now you have a "fat pointer" (pointer and length) on the stack and the data on the heap. It's a subtle change, but if you're storing millions of these in a large collection, saving those 8 bytes per instance adds up quickly, and it signals to other developers that this data is immutable in size.
The Raw Speed of Stack Arrays
When the size is known at compile time, there is no contest: use a primitive array [T; N]. Arrays are stored inline. If you have a struct containing an array, the data is right there in the struct—no heap allocation, no pointers, no indirection. The CPU can fetch the entire struct and the array in a single memory read.
I've seen developers shy away from arrays because they fear the stack overflow, and that's a valid concern for massive datasets. But for small, fixed-size buffers (like a 4x4 matrix for graphics or a 32-byte hash), the stack is orders of magnitude faster. Just remember that arrays must have a constant size; you can't use a variable to define an array's length. If you find yourself wanting a "dynamic array" that is actually quite small, consider using a crate like smallvec, but for pure standard library performance, the array is king.
- Use
[T; N]: When the size is a constant and small enough to fit on the stack. - Use
Box<[T]>: When the size is determined at runtime, but remains fixed, and you want to minimize memory overhead. - Use
Vec<T>: When you need to push, pop, or grow the collection dynamically.
📋 Practical Task
Refactoring the Network Packet Buffer
You are optimizing a network driver. Currently, the code uses Vec<u8> to store incoming packets. However, the hardware specification guarantees that every packet is exactly 1500 bytes. The current implementation is causing excessive heap allocations and slowing down the packet processing loop.
Your Task: Refactor the following code to remove the Vec. Since the size is a known constant (1500), replace the dynamic allocation with a stack-allocated array to improve cache locality and remove heap overhead.
struct Packet {
id: u32,
payload: Vec<u8>,
}
impl Packet {
fn new(id: u32, data: Vec<u8>) -> Self {
// In the real world, we'd check if data.len() == 1500
Packet { id, payload: data }
}
fn get_byte(&self, index: usize) -> u8 {
self.payload[index]
}
}
fn main() {
let raw_data = vec![0u8; 1500];
let packet = Packet::new(1, raw_data);
println!("Byte 0: {}", packet.get_byte(0));
}
Requirements:
1. Change the payload field in Packet to use a fixed-size array of 1500 bytes.
2. Update the new method to accept an array or convert the incoming data into one.
3. Ensure the code still compiles and provides the same get_byte functionality.
There are no comments for now.