1. 程式人生 > 其它 >Rust 中的資料佈局-repr

Rust 中的資料佈局-repr

repr(Rust)

首先,所有型別都有一個以位元組為單位的對齊方式,一個型別的對齊方式指定了哪些地址可以用來儲存該值。一個具有對齊方式n的值只能儲存在n的倍數的地址上。所以對齊方式 2 意味著你必須儲存在一個偶數地址,而 1 意味著你可以儲存在任何地方。對齊至少是 1,而且總是 2 的冪。

基礎型別通常按照其大小對齊,儘管這是特定平臺的行為。例如,在 x86 上u64f64通常被對齊到 4 位元組(32 位)。

一個型別的大小必須始終是其對齊方式的倍數(零是任何對齊方式的有效大小),這就保證了該型別的陣列總是可以通過偏移其大小的倍數來進行索引。注意,在動態大小的型別的情況下,一個型別的大小和對齊方式可能不是靜態的。

Rust 給你提供了以下方式來佈置複合資料。

  • structs (命名複合型別 named product types)
  • tuples (匿名複合型別 anonymous product types)
  • arrays (同質複合型別 homogeneous product types)
  • enums (命名總和型別 —— 有標籤的聯合體 named sum types -- tagged unions)
  • unions (無標籤的聯合體 untagged unions)

如果一個列舉的變體都沒有相關聯的資料,那麼它就被稱為無域

預設情況下,複合結構的對齊方式等於其欄位對齊方式的最大值。因此,Rust 會在必要時插入填充,以確保所有欄位都正確對齊,並且整個型別的大小是其對齊的倍數。比如說:

 

struct A {
    a: u8,
    b: u32,
    c: u16,
}

將在目標上以 32 位對齊,將這些基本型別對齊到它們各自的大小。因此,整個結構的大小將是 32 位的倍數。它可能變成:

 

struct A {
    a: u8,
    _pad1: [u8; 3], // 需要和 `b` 記憶體對齊
    b: u32,
    c: u16,
    _pad2: [u8; 2], // 讓總體的大小是 4 的倍數
}

或者,也許:

 

struct A {
    b: u32,
    c: u16,
    a: u8,
    _pad: u8,
}

所有資料都儲存在結構中,正如你在 C 語言中所期望的那樣。然而,除了陣列(密集包裝且無序)之外,資料的佈局在預設情況下沒有指定。給出以下兩個結構的定義:

 

struct A {
    a: i32,
    b: u64,
}

struct B {
    a: i32,
    b: u64,
}

Rust 確實保證 A 的兩個例項的資料佈局完全相同。然而,Rust 目前並不保證 A 的例項與 B 的例項具有相同的欄位排序或填充。

對於我們編寫的 A 和 B 來說,這一點似乎是迂腐的,但是 Rust 的其他幾個特性使得該語言有必要以複雜的方式來處理資料佈局。

例如,考慮這個結構:

 

struct Foo<T, U> {
    count: u16,
    data1: T,
    data2: U,
}

現在考慮一下Foo<u32, u16>Foo<u16, u32>的單態。如果 Rust 按照指定的順序排列欄位,我們希望它能對結構中的值進行填充以滿足其對齊要求。因此,如果 Rust 不對欄位重新排序,我們希望它能產生以下結果:

 
struct Foo<u16, u32> {
    count: u16,
    data1: u16,
    data2: u32,
}

struct Foo<u32, u16> {
    count: u16,
    _pad1: u16,
    data1: u32,
    data2: u16,
    _pad2: u16,
}

後一種情況很顯然浪費了空間,對空間的最佳利用要求不同的單體有不同的欄位排序

列舉使這種考慮變得更加複雜,直觀地說,一個列舉如下:

 

enum Foo {
    A(u32),
    B(u64),
    C(u8),
}

可能會被佈局成:

 

struct FooRepr {
    data: u64, // 根據 tag 的不同,這一項可以為 u64,u32,或者 u8
    tag: u8,   // 0 = A,1 = B, 2 = C
}

事實上,這大約正是它的佈局方式(根據tag的大小和位置來調整)。

然而,在一些情況下,這樣的表述是低效的。這方面的典型案例是 Rust 的“空指標優化”:一個由單個外部單元變數(例如None)和一個(可能巢狀的)非空指標變數(例如Some(&T))組成的列舉,使得標籤沒有必要。空指標可以安全地被解釋為單位(None)的變體。這導致的結果是,例如,size_of::<Option<&T>>() == size_of::<&T>()

在 Rust 中,有許多型別會包含不可為空的指標,如Box<T>Vec<T>String&T&mut T。同樣地,我們可以想象巢狀的列舉將它們的標記集中到一個單一的欄位中,因為根據定義,它們的有效值範圍有限。原則上,列舉可以使用相當複雜的演算法,在整個巢狀型別中用禁止使用的值來儲存列舉型別。因此,我們今天不指定列舉佈局是特別符合預期的。