Giao diện
Memory Layout & Internals Deep Tech
Hiểu cách Rust tổ chức dữ liệu trong bộ nhớ
Stack vs Heap trong Rust
Nguyên tắc cơ bản
| Đặc điểm | Stack | Heap |
|---|---|---|
| Allocation | Compile-time known size | Runtime dynamic size |
| Speed | Cực nhanh (pointer bump) | Chậm hơn (allocator call) |
| Lifetime | Tự động (function scope) | Manual hoặc Smart Pointer |
| Access | LIFO (Last In, First Out) | Random access |
Khi nào Rust allocate ở đâu?
rust
fn main() {
// === STACK ALLOCATION ===
let x: i32 = 42; // 4 bytes trên stack
let point: (f64, f64) = (1.0, 2.0); // 16 bytes trên stack
let arr: [u8; 100] = [0; 100]; // 100 bytes trên stack
// === HEAP ALLOCATION ===
let vec: Vec<i32> = vec![1, 2, 3]; // Vec metadata trên stack
// Data [1,2,3] trên heap
let s: String = String::from("Hello"); // String metadata trên stack
// "Hello" bytes trên heap
let boxed: Box<i32> = Box::new(42); // Box<i32> (pointer) trên stack
// i32 value 42 trên heap
}Memory Diagram: String Layout
STACK HEAP
┌─────────────────┐ ┌─────────────────────┐
s: String │ ptr ──────────────────────▶ │ H │ e │ l │ l │ o │
├─────────────────┤ └─────────────────────┘
│ len: 5 │ 0 1 2 3 4
├─────────────────┤
│ cap: 5 │
└─────────────────┘
24 bytes 5 bytes
(3 × usize on 64-bit)Giải thích:
ptr: Con trỏ 8 bytes trỏ đến vùng nhớ heaplen: Số bytes đang sử dụng (5 cho "Hello")cap: Capacity - tổng dung lượng allocated (có thể >= len)
Vec Layout
rust
let v: Vec<i32> = vec![10, 20, 30, 40]; STACK HEAP
┌─────────────────┐ ┌─────┬─────┬─────┬─────┐
v: Vec<i32>│ ptr ──────────────────────▶ │ 10 │ 20 │ 30 │ 40 │
├─────────────────┤ └─────┴─────┴─────┴─────┘
│ len: 4 │ 0 4 8 12 (byte offset)
├─────────────────┤
│ cap: 4 │
└─────────────────┘
24 bytes 16 bytes (4 × i32)Sized vs Unsized Types
Sized Trait
Trong Rust, mọi type mặc định đều là Sized — compiler biết size tại compile-time.
rust
// Sized types - compiler biết size lúc compile
let x: i32 = 42; // 4 bytes
let y: (u64, u64) = (1, 2); // 16 bytes
let z: [u8; 100] = [0; 100]; // 100 bytes
// KHÔNG compile - size không biết lúc compile
// let s: str; // ERROR: str is unsized
// let a: [i32]; // ERROR: slice is unsizedDynamically Sized Types (DST)
DST là types mà size chỉ biết lúc runtime:
| DST | Mô tả |
|---|---|
str | String slice (UTF-8 bytes) |
[T] | Slice of T |
dyn Trait | Trait object |
Quy tắc DST:
- DST không thể tồn tại trực tiếp trên stack
- Phải truy cập qua reference (
&str,&[T]) hoặc smart pointer (Box<dyn Trait>)
rust
// ✅ ĐÚNG: DST đằng sau reference
fn print_str(s: &str) { println!("{}", s); }
// ✅ ĐÚNG: DST trong Box
fn process(handler: Box<dyn Fn()>) { handler(); }
// ❌ SAI: DST trực tiếp
// fn bad(s: str) {} // ERROR: the size of `str` cannot be known at compile-timeFat Pointers cho DST
Reference đến DST không phải là 1 pointer, mà là 2 pointers (Fat Pointer):
Regular Pointer (8 bytes) Fat Pointer (16 bytes)
┌─────────────────┐ ┌─────────────────┐
&i32 │ ptr │ &str │ ptr │ → dữ liệu
└─────────────────┘ ├─────────────────┤
│ len (metadata) │ → length
└─────────────────┘rust
use std::mem::size_of;
// Thin pointer
assert_eq!(size_of::<&i32>(), 8); // 8 bytes (pointer only)
assert_eq!(size_of::<&[i32; 4]>(), 8); // 8 bytes (sized array)
// Fat pointer
assert_eq!(size_of::<&str>(), 16); // 16 bytes (ptr + len)
assert_eq!(size_of::<&[i32]>(), 16); // 16 bytes (ptr + len)
assert_eq!(size_of::<&dyn std::fmt::Debug>(), 16); // 16 bytes (ptr + vtable ptr)Alignment & Padding
Alignment Rules
CPU đọc memory hiệu quả nhất khi data được aligned — địa chỉ chia hết cho size của type.
| Type | Size | Alignment |
|---|---|---|
u8, i8 | 1 byte | 1 byte |
u16, i16 | 2 bytes | 2 bytes |
u32, i32, f32 | 4 bytes | 4 bytes |
u64, i64, f64 | 8 bytes | 8 bytes |
usize, pointer | 8 bytes (64-bit) | 8 bytes |
Struct Padding
Compiler thêm padding bytes để đảm bảo alignment:
rust
// Không tối ưu - có padding
struct BadLayout {
a: u8, // 1 byte
// [3 bytes padding]
b: u32, // 4 bytes
c: u8, // 1 byte
// [3 bytes padding]
}
// Total: 12 bytes (thay vì 6 bytes thực tế)
// Tối ưu - sắp xếp theo size giảm dần
struct GoodLayout {
b: u32, // 4 bytes (aligned at 0)
a: u8, // 1 byte
c: u8, // 1 byte
// [2 bytes padding để struct alignment = 4]
}
// Total: 8 bytesMemory Layout Diagram:
BadLayout (12 bytes):
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ a │ pad │ pad │ pad │ b │ b │ b │ b │ c │ pad │ pad │ pad │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
0 1 2 3 4 5 6 7 8 9 10 11
GoodLayout (8 bytes):
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ b │ b │ b │ b │ a │ c │ pad │ pad │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
0 1 2 3 4 5 6 7Repr Attributes
rust
// #[repr(C)] - Layout như C, predictable
#[repr(C)]
struct FFIStruct {
a: u8,
b: u32,
c: u8,
}
// Giữ nguyên order, có padding như C compiler
// #[repr(packed)] - Không padding, có thể misaligned
#[repr(packed)]
struct PackedStruct {
a: u8,
b: u32, // ⚠️ Có thể unaligned!
c: u8,
}
// Total: 6 bytes, nhưng access b có thể chậm hơn
// #[repr(align(N))] - Minimum alignment
#[repr(align(64))]
struct CacheAligned {
data: [u8; 32],
}
// Struct aligned to 64-byte cache line boundary⚠️ CẢNH BÁO
#[repr(packed)] có thể gây undefined behavior trên một số CPU architectures khi truy cập unaligned data. Chỉ sử dụng khi cần thiết cho FFI hoặc binary protocols.
Zero-Sized Types (ZST)
Types có size = 0 bytes, nhưng vẫn tồn tại trong type system:
rust
use std::mem::size_of;
// ZST examples
struct Empty;
struct Marker;
struct PhantomData<T>(std::marker::PhantomData<T>);
assert_eq!(size_of::<()>(), 0);
assert_eq!(size_of::<Empty>(), 0);
assert_eq!(size_of::<[u8; 0]>(), 0);
// ZST không chiếm memory nhưng có type information
let _: Vec<Empty> = vec![Empty, Empty, Empty];
// Vec header 24 bytes, data 0 bytes!Use case: Marker types cho type-state pattern, PhantomData cho variance.
Enum Layout
Rust tối ưu enum layout rất tốt:
rust
use std::mem::size_of;
// Simple enum - 1 byte discriminant đủ cho 256 variants
enum Direction {
North, South, East, West
}
assert_eq!(size_of::<Direction>(), 1);
// Enum with data - discriminant + largest variant
enum Event {
Click { x: i32, y: i32 }, // 8 bytes
KeyPress(char), // 4 bytes
Quit, // 0 bytes
}
assert_eq!(size_of::<Event>(), 12); // 4 (discriminant) + 8 (largest)
// Option<&T> - Null Pointer Optimization (NPO)
assert_eq!(size_of::<Option<&i32>>(), 8); // Same as &i32!
// None represented as null pointer, no extra discriminant needed
// Option<NonZeroU64> - NPO applies
use std::num::NonZeroU64;
assert_eq!(size_of::<Option<NonZeroU64>>(), 8); // Same as u64!Null Pointer Optimization Diagram:
Option<&T> Layout (8 bytes thanks to NPO):
Some(&value): ┌─────────────────────────┐
│ 0x7fff_1234_5678 (ptr) │
└─────────────────────────┘
None: ┌─────────────────────────┐
│ 0x0000_0000_0000 (null)│
└─────────────────────────┘
Without NPO would be 16 bytes:
┌──────────┬──────────────┐
│ tag: 0/1 │ ptr or undef │
└──────────┴──────────────┘Bảng Tóm tắt
rust
use std::mem::{size_of, align_of};
// === SIZE & ALIGNMENT ===
// Primitives
assert_eq!(size_of::<u8>(), 1);
assert_eq!(size_of::<u64>(), 8);
assert_eq!(align_of::<u64>(), 8);
// Pointers
assert_eq!(size_of::<&i32>(), 8); // Thin pointer
assert_eq!(size_of::<&str>(), 16); // Fat pointer
assert_eq!(size_of::<Box<i32>>(), 8); // Box is pointer-sized
// Collections on stack
assert_eq!(size_of::<String>(), 24); // ptr + len + cap
assert_eq!(size_of::<Vec<i32>>(), 24); // ptr + len + cap
// Null Pointer Optimization
assert_eq!(size_of::<Option<&i32>>(), size_of::<&i32>());
assert_eq!(size_of::<Option<Box<i32>>>(), size_of::<Box<i32>>());