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`。类似地,可以想象嵌套枚举将其标签合并到一个判别式中,因为根据定义,它们已知具有有限范围的有效值。原则上,枚举可以使用相当复杂的算法将位存储在嵌套类型中,并使用禁止的值。因此,我们*特别*希望今天不要指定枚举布局。