W Rust kompilator stara się efektywnie rozmieszczać dane w pamięci, używając wiedzy o wyrównaniu i możliwościach układu struktur i enum. Pytanie to jest szczególnie aktualne w niskopoziomowym i systemowym programowaniu, w którym nadmierny rozmiar typu prowadzi do znacznego marnotrawstwa pamięci.
Automatyczne wyrównanie struktur to cecha większości języków, jednak w Rust kompilator zapewnia ścisłe gwarancje co do układu (przy czym dopuszcza jego optymalizację), a w przypadku enum realizuje kompaktowe przechowywanie poprzez łączenie pamięci dla wszystkich wariantów, uwzględniając maksymalny rozmiar).
Kolejność i typy pól w strukturze lub enum wpływają na ostateczny rozmiar typu z powodu cech wyrównania. Nieprawidłowa kolejność zwiększa „padding” — nieużywane bajty. W enum z danymi skojarzonymi maksymalny wariant określa rozmiar, a niektóre konstrukcje mogą sprawić, że enum stanie się nieoczekiwanie przestrzenny.
Prawidłowo określać kolejność pól i wybierać typy, analizować rozmiar danych przez std::mem::size_of. W przypadku enum — być ostrożnym z zagnieżdżonymi strukturami i wskaźnikami.
Przykład kodu:
struct Bad { a: u8, // zajmuje 1 bajt + 7 bajtów paddingu b: u64, // zajmuje 8 bajtów } struct Good { b: u64, // 8 bajtów, wyrównanie od początku a: u8, // 1 bajt + 7 bajtów paddingu na końcu }
Sprawdzenie rozmiaru:
use std::mem::size_of; println!("{}", size_of::<Bad>()); // 16 bajtów println!("{}", size_of::<Good>()); // 16 bajtów (ale padding teraz na końcu)
Dla enum:
enum Example { Unit, Num(u32), Pair(u64, u8), } println!("{}", size_of::<Example>()); // rozmiar — max(rozmiar wariantów) + discriminant
Kluczowe cechy:
Czy zmiana kolejności pól u8 i u64 w strukturze zmieni rozmiar struktury?
Nie, całkowity rozmiar wciąż będzie wielokrotnością wyrównania największego pola, ale padding się przesunie. To ważne, jeśli struktura jest włączana w inną strukturę lub przekazywana do FFI.
Czy mały enum może mieć duży rozmiar pamięci?
Tak, jeśli przynajmniej jeden wariant zawiera duży obiekt lub wskaźnik, całkowity rozmiar enum będzie odpowiadał „najcięższemu” wariantowi plus discriminant.
Czy układ struktury jest zawsze taki sam na wszystkich platformach?
Nie, układ i wyrównanie mogą się różnić między architekturami. Do ścisłej kontroli używa się atrybutu repr(C).
#[repr(C)] struct MyFFIStruct { x: u32, y: u8, }
W dużych kolekcjach używa się enum z zagnieżdżonym Vec, który rzadko występuje, ale zwiększa rozmiar enum dziesięciokrotnie. Pamięć jest marnowana.
Zalety:
Wady:
Enum podzielony na kilka mniejszych enum, tablice/kolekcje są przechowywane osobno lub przez Box dla rzadkich wariantów, układ kontrolowany za pomocą #[repr(C)]. Rozmiar sprawdzany przez size_of.
Zalety:
Wady: