Każdy kto pisze w Rust szybko trafia na Vec.

Tworząc wektor przez Vec::new() można by się spodziewać, że gdzieś w tle wywołuje się malloc. Sprawdziłem — nie wywołuje.

Zajrzałem do źródeł żeby zrozumieć dlaczego.

Vec::new()

Przyjrzyjmy się poniższemu listingowi:

fn inspect<T>(v: &Vec<T>) {
    println!("ptr: {:p}", v.as_ptr());
    println!("cap: {}", v.capacity());
    println!("len: {}", v.len());
}

fn main() {
    let v: Vec<i32> = Vec::new();
    inspect(&v);
}

Wynikowa wartość którą otrzymujemy to:

ptr: 0x4
cap: 0
len: 0

Jaką drogę przebył nasz kod? Spróbujmy zdebugować runtime Rusta!

backtrace

Wiemy gdzie nasza funkcja docelowo ląduje — a kod źródłowy prezentuje się tak: Vec::new_in na GitHubie

#[inline]
const fn new_in(alloc: A, align: Alignment) -> Self {
    let ptr = Unique::from_non_null(NonNull::without_provenance(align.as_nonzero_usize()));
    // `cap: 0` means "unallocated". zero-sized types are ignored.
    Self { ptr, cap: ZERO_CAP, alloc }
}

backtrace

Powyższy kod już po komentarzu pokazuje zasadę której hołduje — Zero Cost Abstractions.

Rust inicjalizując wektor robi to najmniejszym kosztem: przygotowuje dangling pointer (doc) (ptr: 0x4), ale faktycznie bez potrzeby nie alokuje pamięci.

Zatem zmodyfikujmy nasz program, dodajmy element i zobaczmy co się zmieni:

fn inspect<T>(v: &Vec<T>) {
    println!("ptr: {:p}", v.as_ptr());
    println!("cap: {}", v.capacity());
    println!("len: {}", v.len());
    println!();
}

fn main() {
    let mut v: Vec<i32> = Vec::new();
    inspect(&v);
    v.push(1);
    inspect(&v);
}

Output:

ptr: 0x4
cap: 0
len: 0

ptr: 0x559967af6d50
cap: 4
len: 1

Jak widać, dopiero po push faktycznie została przydzielona pamięć.

Z tego krótkiego artykułu udało nam się dowiedzieć, że Vec::new() to przykład filozofii Rusta — płacisz tylko za to czego używasz. (zero-cost abstractions)