Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1.14 内存中的类型 Pt.2:动态大小的类型和宽指针(Wide Pointer)、无填充内存布局、给特定字段或类型更大的对齐、复杂类型的内存表示、repr(Rust)

1.14.1. repr(Rust)

还记得我们上一篇文章的例子吗?那个例子使用repr(C),而C的表示的限制在于需要将所有的字段按原struct定义的顺序放置。

repr(Rust)是默认表示。它有意比repr(C)提供更少的布局保证:编译器可以重排字段,而且即便两个类型拥有相同字段、相同字段类型、相同定义顺序,也不能保证它们有相同的内存布局。

因为编译器可以重排字段(例如把较大的字段放在前面),填充常常可以减少。对于上一篇文章Foo例子,有一种可能的优化布局就不需要填充。

对布局的保证少了,编译器就有余地进行重新编排,从而产生高效的代码。

如果使用repr(Rust),那么上文的Foo结构体在内存中的一种可能布局是:

代码字段类型的大小默认内存表示填充最终对齐
#[repr(Rust)]
struct Foo {
long: u64,8 bytes8 bytes 对齐8 bytes
normal: u32,4 bytes4 bytes 对齐
short: u16,2 bytes2 bytes 对齐
small: u8,1 byte1 byte 对齐
tiny: bool,1 bit1 byte 对齐
}
共计 16 bytes
  • 编译器首先按照字段类型的大小进行排列,把最大的放在前面,这样就可以决定它是按什么对齐的(这个例子中u64是最大的,占8字节,所以就以8字节对齐)
  • 编译器往后面一看剩下的所有字段加一起正好是8字节,那么就会让他们连着放在一起,就可以避免填充
  • 最终这个例子的结构体只需要16字节就可以了,比使用repr(C)直接节约了一半的内存空间
  • 这样更加高效,但是编译时间可能会长一点

1.14.2. 无填充布局

你可以告诉编译器字段间无需任何填充,但是你需要承担不对齐访问造成的性能损失。

在内存有限,类型实例较多的时候就可以使用无填充布局。或者是通过低带宽网络连接发送内存表示时。

启用无填充布局需要在类型上需要添加*[repr(packed)]注解。

注意,使用无填充布局:

  • 可能会导致代码运行速度变慢
  • 极端情况下,如果CPU只支持对齐操作,可导致程序崩溃

1.14.3. 给特定字段或类型更大的对齐

使用#[repr(align(n))]注解可以给特定字段或类型更大的对齐,其中n是参数。

例如:保证在内存中连续(相邻)存储(就像数组)的不同值最终位于CPU上不同的缓存上,就可以避免伪共享(false sharing)

简单解释一下相关的术语:

  • 缓存是由缓存行组成的,缓存都是以缓存行作为一个单位来处理的。缓存行(cache line) 是可以映射到缓存中的最小数据部分
  • 伪共享指的是两个不同的CPU访问共享同一个缓存行的不同变量时,就发生了伪共享。理论上它们可以并行操作,但最终它们都争相更新缓存中的同一个条目。它可能导致并发类程序中的巨大性能降级

1.14.4. 复杂类型的内存表示

  • 元组(Tuple,详见【Rust指南】3.3. 数据类型:复合类型):在内存中的表示就像结构体,其字段类型与元组元素类型按顺序相同
  • 数组:所包含的类型的连续序列,元素间没有填充
  • Union:对于每一个字段来说,其布局的选择是独立的;对齐就是所有字段里最大的那个
  • 枚举:和Union一样,额外有一个隐藏的共享字段,用于存储枚举变体的鉴别符。代码用鉴别符的值来判定给定值所含的是哪一个变体,鉴别符的大小取决于变体的数量

1.14.5. 动态大小的类型和宽指针

Rust中大多数类型自动实现了Sized trait,关于它的详细介绍见【Rust指南】19.5.4. 动态大小类型与Sized trait,这里做一个简单的回顾。

Rust 需要了解有关其类型的某些详细信息,例如为特定类型的值分配多少空间。这使得动态大小类型(dynamically sized types) 这个概念有些迷惑人。它有时被称为DSTunsized types,这些类型允许我们使用只能在运行时知道其大小的值来编写代码。

为了使用动态大小类型,Rust提供了Sized trait来确定类型的大小在编译时是否已知。对于编译时大小已知的所有内容,都会自动实现此trait。此外,Rust隐式地​​为每个泛型函数添加了Sized trait。默认情况下,泛型函数仅适用于编译时大小已知的类型。但是也可以使用?Sized来放宽此限制。?Sized意味着“ T可能实现也可能没实现Sized trait”,也就是T可能是动态大小类型也可能不是。这种表示方法不需要泛型类型在编译时必须具有已知大小这个默认条件。有这种含义的?Trait语法仅适用于Sized trait ,没有任何其他trait。

当函数需要接受DST(例如trait对象、切片等)作为参数时怎么办呢?我们可以使用宽指针(wide pointer,或者叫fat pointer)

1.14.6. 宽指针(Wide Pointer)

通过将非Sized类型放在宽指针后面,就可以弥补Sized和非Sized类型之间的差距。

那么到底什么是宽指针呢?宽指针就是普通指针附加一个“字大小(word-size)“的字段。它可以提供给编译器所需要的关于指针的额外信息,以生产使用该指针的合理代码。

当你引用DST时,编译器会自动为你组建一个宽指针。比如说切片(Slice) 的附加信息就是切片的长度。

宽指针是Sized是因为它本质上是指针,大小是固定的(usized的2倍——一个字段用于存储指针,另一个用于存储附带信息,用于“完善该类型”)。

备注:Box<T>Arc<T>都支持存储宽指针,所以它们都支持?Sized