repr(Rust)
首先和最重要的,所有的类型都有一个基于字节的对齐方式。一个类型的对齐方式指定了储存值的有效地址。一个n
字节对齐的值必须储存在n
倍数的地址上。所以2对齐的意味着你必须储存在偶数地址,而1则表示你可以随便存哪里。对齐值最少是1(字节),并总是2的幂(2, 4, 8…)。大多数原始类型通常以它们的大小对齐,尽管这是平台相关的行为。特别的,在x86平台上u64
和f64
可能只能以32位对齐。
一个类型的大小必须总是它对齐值的倍数。这保证了一个这个类型的数组总是可以通过位移乘以它的大小来索引。注意在动态大小类型)情况下大小和对齐值可能不能静态的获取到。
Rust给出如下方式来布局复合类型:
- 结构体(命名product type)
- 元组(匿名product type)
- 数组(同质product type)
- 枚举(命名集合类型 — 标记的联合)
一个枚举在没有一个变量有关联数据时被认为与C语言相似的。
复合结构将拥有一个等于它的字段最大对齐值的对齐值。因此Rust将会在必要时插入填充来保证所有字段正确的对齐和整体的类型大小是它对齐值的倍数。例如:
struct A {
a: u8,
b: u32,
c: u16,
}
将是32位对齐的,假设这些原始类型都对齐到他们的大小。因此它的大小将是32位的倍数。潜在的它真正的样子将是:
struct A {
a: u8,
_pad1: [u8; 3], // to align `b`
b: u32,
c: u16,
_pad2: [u8; 2], // to make overall size multiple of 4
}
这些类型之间没有间隔;所有数据都是连续储存的,正如你在C语言里期待的那样。然而数组是一个例外(它们是紧密打包并连续的),数据的布局默认并不由Rust指定。给定如下两个结构体定义:
struct A {
a: i32,
b: u64,
}
struct B {
x: 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,
}
后一个情况看起来很浪费空间。因此一个对空间的优化利用需要对不同的单态拥有不同的字段排序。
注意:这是一个在Rust 1.0中尚未实现的假设优化
枚举使这样的考量变得更复杂。一个这样的枚举:
enum Foo {
A(u32),
B(u64),
C(u8),
}
将会想当然的布局为:
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的“空指针优化”。给定一个已知为空的指针(例如&u32
),一个枚举可以在指针内存储一个判别位,通过把空作为一个特殊值。最终的结果是size_of::<Option<&T>>() == size_of::<&T>()
。
Rust中的很多类型是,或者包含“非空”指针,例如Box<T>
,Vec<T>
,String
,&T
和&mut T
。相似的,你可以想象的到嵌套的枚举(带有值的枚举)将他们的标记存入一个判别式池中,因为通过定义可知他们只有有限范围的有效值。原则上枚举可以使用相当复杂的算法通过受限的表现形式在嵌套的类型中缓存位。为此,目前我们并未指定枚举的布局是特别理想的。