完整標題: [進階 Rust] 1.14. 記憶體型別 Pt.2 - 動態大小型別與寬指標、封裝佈局、特定欄位或型別的較大對齊、複雜型別的記憶體表示,以及 Repr Rust

1.14.1. repr(Rust)

還記得前一篇文章的範例嗎?那個範例使用了 repr(C),而 C 表示法的限制是,所有欄位必須依照原始結構體中定義的順序放置。

repr(Rust) 是預設表示法。它刻意提供比 repr(C) 更少的佈局保證:編譯器可以重新排序欄位,而且即使兩個型別擁有相同順序的相同欄位,也無法保證它們會共用相同的佈局。

因為編譯器可以重新排序欄位(例如,把較大的欄位放在前面),所以通常可以減少填充。在前一篇文章的 Foo 範例中,有一種可能的優化佈局不需要任何填充。

由於對佈局的保證較少,編譯器有空間重新排列內容並產生更有效率的程式碼。

如果使用 repr(Rust),則上述 Foo 結構體的一種可能記憶體佈局如下:

程式碼 欄位型別大小 預設表示法 填充 最終對齊
#[repr(Rust)]
struct Foo {
long: u64, 8 bytes 8-byte aligned 8 bytes
normal: u32, 4 bytes 4-byte aligned
short: u16, 2 bytes 2-byte aligned
small: u8, 1 byte 1-byte aligned
tiny: bool, 1 byte 1-byte aligned
}
總計 16 bytes
  • 編譯器首先依大小排列欄位,把最大的欄位放在最前面,以便決定結構體應使用的對齊方式。在本例中,u64 是最大的欄位,佔 8 bytes,因此結構體會對齊到 8 bytes。
  • 接著,編譯器檢視剩餘欄位,發現它們的總大小正好是 8 bytes,因此可以把它們放在一起而無需填充。
  • 最終,這個結構體只需要 16 bytes,與 repr(C) 相比節省了一半的記憶體。
  • 這更有效率,但編譯時間可能會稍微增加。

1.14.2. 封裝佈局

你可以告訴編譯器欄位之間不需要填充,但必須接受未對齊存取所帶來的效能代價。

當記憶體有限,或當某型別有許多實例時,封裝佈局會很有用。當透過低頻寬網路連線傳送記憶體表示時也很實用。

要啟用封裝佈局,請在型別上加上 #[repr(packed)] 註解。

請注意,使用封裝佈局時:

  • 程式碼可能執行得更慢
  • 在極端情況下,若 CPU 僅支援對齊存取,程式可能會當機

1.14.3. 給特定欄位或型別較大的對齊

使用 #[repr(align(n))] 註解,可以給特定欄位或型別較大的對齊,其中 n 是參數。

例如,若想確保連續存放在記憶體中的不同值(如陣列中的值)落在不同的 CPU 快取行上,就可以避免偽共享

以下是相關術語的簡要說明:

  • 快取由快取行組成,快取以快取行為單位運作。快取行 是能映射到快取中的最小資料單位。
  • 偽共享是指兩個不同的 CPU 存取同一個快取行中的不同變數。理論上它們可以平行運作,但最後卻都在競爭更新同一個快取項目。這可能導致並行程式的效能大幅下降。

1.14.4. 複雜型別的記憶體表示

  • 元組:其記憶體表示類似結構體;欄位型別與元組元素型別的順序相同。
  • 陣列:所包含型別的連續序列,元素之間沒有填充。
  • 聯集:每個欄位的佈局選擇是獨立的;對齊方式是所有欄位中的最大值。
  • 列舉:類似聯集,但額外有一個隱藏的共用欄位,用來儲存列舉變體的判別值。程式碼會使用判別值來決定給定值包含哪個變體。判別值的大小取決於變體的數量。

1.14.5. 動態大小型別與寬指標

Rust 中的大多數型別會自動實作 Sized trait。

Rust 需要知道其型別的一些細節,例如為特定型別的值分配多少空間。這就是讓「動態大小型別」的概念有點令人困惑的原因。它們有時被稱為 DSTunsized types,讓我們可以撰寫處理大小僅在執行時期才知道的值的程式碼。

為了使用動態大小型別,Rust 提供 Sized trait 來表示型別的大小是否在編譯時期已知。所有在編譯時期已知大小的型別都會自動實作此 trait。Rust 也會隱式地將 Sized trait 加到每個泛型函式中。預設情況下,泛型函式僅適用於大小在編譯時期已知的型別。 這個限制可以透過 ?Sized 放寬。?Sized 的意思是「T 可能實作也可能不實作 Sized」,也就是 T 可能是不定大小型別。此語法不會要求「泛型型別必須在編譯時期有已知大小」這個預設條件。?Trait 這種語法僅適用於 Sized trait,不適用於其他 trait。

當函式需要接受 DST(例如 trait 物件或切片)作為參數時,該怎麼辦?我們可以使用寬指標(也稱為胖指標)。

1.14.6. 寬指標

透過在寬指標後面放置非 Sized 型別,我們可以彌合 Sized 與非 Sized 型別之間的差距。

那麼寬指標究竟是什麼?寬指標是一個普通的指標,再附加一個「word-size」欄位。它為編譯器提供使用該指標所需的額外資訊,使其能產生使用該指標的合理程式碼。

當你參照一個 DST 時,編譯器會自動為你建立寬指標。例如,切片 的額外資訊就是切片的長度。

寬指標是 Sized,因為它們本質上是指標,而且大小是固定的(是一般「有大小」指標的兩倍——一個欄位儲存指標本身,另一個欄位儲存用來「完成」型別的附加中繼資料)。

注意:Box<T>Arc<T> 都支援儲存寬指標,因此它們都支援 ?Sized