Rust repr(Rust)
底层编程经常需要关注数据布局。每种类型都有一个数据对齐属性(alignment)。一种类型的对齐属性决定了哪些内存地址可以合法地存储该类型的值。如果对齐属性是n,那么它的值的存储地址必须是n的倍数。所以,对齐属性2表示值只能存储在偶数地址里,1表示值可以存储在任何的地方。对齐属性最小为1,并且永远是2的整数次幂。虽然不同平台的行为可能会不同,但大部分情况下基础类型都是按照它的类型大小对齐的。特别的是,在x86平台上u64
和f64
都是按照32位对齐的。
一种类型的大小都是它对齐属性的整数倍,这保证了这种类型的值在数组中的偏移量都是其类型尺寸的整数倍,可以按照偏移量进行索引。需要注意的是,动态尺寸类型的大小和对齐可能无法静态获取。
Rust有如下几种复合类型:
- 结构体(带命名的复合类型 named product types)
- 元组(匿名的复合类型 anonymous product types)
- 数组(同类型数据集合 homogeneous product types)
- 枚举(带命名的标签联合体 named sum types -- tagged 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的倍数
// (译注:原文就是“4的倍数”,但似乎“32的倍数”才对)
}
这里所有的类型都是直接存储在结构体中的,成员类型和结构体之间没有其他的中介。这一点和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
}
这也确实就是一般情况下枚举的布局方式。
但是,在很多情况下这种表达方式并不是效率最高的。一个典型场景就是Rust的“null指针优化”:如果一个枚举类型只包含一个单值变量(比如None
)和一个(级联的)非null指针变量(比如&T
),那么tag其实是不需要的,因为那个单值变量完全可以用null指针来表示。所以,size_of::<Option<&T>>() == size_of::<&T>()
,这个比较的结果是正确的。
Rust中的许多类型都包含或者本身就是非null指针,比如Box<T>
,Vec<T>
,String
,&T
以及&mut T
。同样的,你或许也能想到,对于级联的枚举类型,Rust会把多个tag变量合并为一个,因为它们本来就只有几个有限的可能取值。大体说来,枚举类型会运用复杂的算法确定各种级联类型的二进制表达方法。因为这件事很重要,我们把枚举的问题留到后面讨论。
下一章:Rust 类型中的奇行种
大部分情况下,我们考虑的都是拥有固定的正数尺寸的类型。但是,并非所有类型都是这样。 1. 动态尺寸类型(DST, Dynamically Sized Type)Rust支持动态尺寸类型,即不能静态获取尺寸 ...