repr(Rust)
首先,所有类型都有一个以字节为单位指定的对齐方式。类型的对齐方式指定了存储该值的有效地址。对齐方式为 n
的值必须仅存储在 n
的倍数的地址处。因此,对齐方式为 2 意味着你必须存储在偶数地址,而 1 意味着你可以存储在任何地方。对齐方式至少为 1,并且始终是 2 的幂。
基本类型通常与其大小对齐,但这取决于平台的具体行为。例如,在 x86 上,u64
和 f64
通常与 4 字节(32 位)对齐。
类型的大小必须始终是其对齐方式的倍数(对于任何对齐方式,零都是有效的大小)。这确保了该类型的数组始终可以通过偏移其大小的倍数来索引。请注意,在动态大小类型的情况下,类型的大小和对齐方式可能是静态未知的。
Rust 为你提供了以下几种布局复合数据的方式
- 结构体(命名的乘积类型)
- 元组(匿名的乘积类型)
- 数组(同质的乘积类型)
- 枚举(命名的和类型——带标签的联合)
- 联合(无标签的联合)
如果枚举的变体都没有关联的数据,则称该枚举为无字段的。
默认情况下,复合结构的对齐方式等于其字段对齐方式的最大值。因此,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], // to align `b` b: u32, c: u16, _pad2: [u8; 2], // to make overall size multiple of 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, // this is either a u64, u32, or u8 based on `tag` 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
。类似地,可以想象嵌套枚举将其标记池化为单个判别式,因为根据定义,它们已知具有有限的有效值范围。原则上,枚举可以使用相当复杂的算法来在具有禁用值的嵌套类型中存储位。因此,我们今天不指定枚举布局是特别可取的。