Vec<T> to dynamiczna, rosnąca tablica, przechowująca elementy w jednym alokowanym (w stosie) bloku pamięci. Dodanie nowego elementu (push) zwiększa długość, a w razie potrzeby — przydzielana jest nowa pamięć (realokacja). Przy push wewnętrznie następuje zwiększenie pojemności w sposób wykładniczy, aby uniknąć ciągłych realokacji. Podczas usuwania elementu (pop/remove) pojemność nie zmniejsza się automatycznie.
Częstym problemem są nadmiarowe alokacje i realokacje przy ciągłym dodawaniu.
Przykład pracy z wstępnym przydzielaniem:
let mut v = Vec::with_capacity(1000); for i in 0..1000 { v.push(i); } assert_eq!(v.capacity(), 1000);
Pytanie: Co się stanie z pojemnością vec po wywołaniu v.shrink_to_fit()? Czy będzie równa długości?
Błędna odpowiedź: Tak, zawsze, po shrink_to_fit pojemność == len.
Poprawna odpowiedź: Niekoniecznie, implementacja shrink_to_fit to "zalecenie" dla alokatora. Zwykle dąży do możliwie minimalnej pojemności, jednak mogą wystąpić różnice w zależności od implementacji alokatora (na przykład, może pozostać powyżej długości).
Przykład:
let mut v = Vec::with_capacity(10); for i in 0..5 { v.push(i); } v.shrink_to_fit(); // pojemność ≥ len (5), ale nie gwarantowane, że == len
Historia
Programista wielokrotnie dodawał obiekty do Vec bez ustalania pojemności, co prowadziło do wykładniczego wzrostu realokacji przy dużych ilościach danych, spowalniając cały proces w "ciężkich" pętlach. Optymalizacja z
with_capacityzmniejszyła czas o 10 razy.
Historia
Zespół próbował zaoszczędzić pamięć poprzez regularne wywoływanie
shrink_to_fitpo każdym pop(). W rezultacie powstały cykle nieustannych realokacji/zwolnień, co obniżyło wydajność i niemal doprowadziło usługę do DoS.
Historia
Zostawili Vec jako pole struktury z wewnętrznymi odniesieniami do jego elementów. Po realokacji (push ponad pojemność) odniesienia zostały unieważnione – powstałe błędy były trudne do wykrycia aż do uruchomienia w produkcji.