repr(Rust)
首先,所有类型都有一个以字节为单位的对齐方式,一个类型的对齐方式指定了哪些地址可以用来存储该值。一个具有对齐方式n
的值只能存储在n
的倍数的地址上。所以对齐方式 2 意味着你必须存储在一个偶数地址,而 1 意味着你可以存储在任何地方。对齐至少是 1,而且总是 2 的幂。
基础类型通常按照其大小对齐,尽管这是特定平台的行为。例如,在 x86 上u64
和f64
通常被对齐到 4 字节(32 位)。
一个类型的大小必须始终是其对齐方式的倍数(零是任何对齐方式的有效大小),这就保证了该类型的数组总是可以通过偏移其大小的倍数来进行索引。注意,在动态大小的类型的情况下,一个类型的大小和对齐方式可能不是静态的。
Rust 给你提供了以下方式来布置复合数据。
- structs (命名复合类型 named product types)
- tuples (匿名复合类型 anonymous product types)
- arrays (同质复合类型 homogeneous product types)
- enums (命名总和类型 —— 有标签的联合体 named sum types -- tagged unions)
- unions (无标签的联合体 untagged unions)
如果一个枚举的所有变体都没有相关联的数据,那么它就被称为无字段(field-less)。
默认情况下,复合结构的对齐方式等于其字段对齐方式的最大值。因此,Rust 会在必要时插入填充,以确保所有字段都正确对齐,并且整个类型的大小是其对齐的倍数。比如说:
#![allow(unused)] fn main() { struct A { a: u8, b: u32, c: u16, } }
将在目标上以 32 位对齐,将这些基本类型对齐到它们各自的大小。因此,整个结构的大小将是 32 位的倍数。它可能变成:
#![allow(unused)] fn main() { struct A { a: u8, _pad1: [u8; 3], // 需要和 `b` 内存对齐 b: u32, c: u16, _pad2: [u8; 2], // 让总体的大小是 4 的倍数 } }
或者:
#![allow(unused)] fn main() { struct A { b: u32, c: u16, a: u8, _pad: u8, } }
所有数据都如同C语言中的一样,直接存储在结构里。然而,除了数组(密集排列且有序)之外,数据的布局在默认情况下都不是确定的。给出以下两个结构的定义:
#![allow(unused)] fn main() { struct A { a: i32, b: u64, } struct B { a: i32, b: u64, } }
Rust 确实保证 A 的两个实例的数据布局完全相同。然而,Rust 目前并不保证 A 的实例与 B 的实例具有相同的字段排序或填充。
对于我们编写的 A 和 B 来说,这一点似乎是没有必要的,但是 Rust 的其他几个特性使得该语言有必要以复杂的方式来处理数据布局。
例如,考虑这个结构:
#![allow(unused)] fn main() { 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,
}
后一种情况很显然浪费了空间,更高效地利用空间要求不同的单体有不同的字段排序。
枚举使情况变得更加复杂,直观地说,一个枚举如下:
#![allow(unused)] fn main() { enum Foo { A(u32), B(u64), C(u8), } }
可能会被布局成:
#![allow(unused)] fn main() { 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
。同理,我们可以想象嵌套的枚举将它们的标记集中到一个单一的字段中,因为根据定义,它们的有效值范围有限。原则上,枚举可以使用相当复杂的算法,在整个嵌套类型中用禁止使用的值来存储枚举类型。因此,我们不指定枚举布局是特别值得的。