完整標題: [進階 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 需要知道其型別的一些細節,例如為特定型別的值分配多少空間。這就是讓「動態大小型別」的概念有點令人困惑的原因。它們有時被稱為 DST 或 unsized 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。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.