完整标题: [高级 Rust] 1.14. 内存类型 第2部分 - 动态大小类型与宽指针、紧凑布局、特定字段或类型的大对齐方式、复杂类型的内存表示以及 Repr Rust

1.14.1. repr(Rust)

还记得上一篇文章中的示例吗?那个示例使用了 repr(C),而 C 表示法的限制在于所有字段必须按照结构体中定义的顺序排列。

repr(Rust) 是默认的表示方式。它有意提供了比 repr(C) 更少的布局保证:编译器可能会重新排列字段,并且即使两个类型具有相同顺序的相同字段,也不保证它们共享相同的布局。

由于编译器可能会重新排列字段(例如,将较大的字段放在前面),因此通常可以减少填充。在上一篇文章的 Foo 示例中,一种可能的优化布局不需要任何填充。

由于布局保证较少,编译器有空间重新排列内容并生成高效的代码。

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

代码 字段类型大小 默认表示方式 填充 最终对齐方式
#[repr(Rust)]
struct Foo {
long: u64, 8 字节 8 字节对齐 8 字节
normal: u32, 4 字节 4 字节对齐
short: u16, 2 字节 2 字节对齐
small: u8, 1 字节 1 字节对齐
tiny: bool, 1 字节 1 字节对齐
}
总计 16 字节
  • 编译器首先按大小对字段进行排序,将最大的字段放在前面,以便确定结构体的对齐方式。在本例中,u64 是最大的字段,占用 8 字节,因此结构体按 8 字节对齐
  • 编译器随后查看剩余字段,发现它们的总大小正好是 8 字节,因此可以将它们放在一起,避免填充
  • 最终,该结构体只需要 16 字节,与 repr(C) 相比节省了一半内存
  • 这样更高效,但编译时间可能会稍长

1.14.2. 紧凑布局

你可以告诉编译器字段之间不需要填充,但你必须接受未对齐访问带来的性能开销。

当内存有限或某个类型的实例数量很多时,紧凑布局可能很有用。当通过低带宽网络连接发送内存表示时,它也很有用。

要启用紧凑布局,请向类型添加 #[repr(packed)] 注解。

注意,使用紧凑布局时:

  • 代码运行速度可能会变慢
  • 在极端情况下,如果 CPU 仅支持对齐访问,程序可能会崩溃

1.14.3. 为特定字段或类型指定更大的对齐方式

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

例如,如果你想确保存储在内存中的不同值(例如在数组中)落在不同的 CPU 缓存行上,你可以避免伪共享

以下是对相关术语的简要解释:

  • 缓存由缓存行组成,缓存以缓存行为单位进行操作。缓存行 是可以映射到缓存中的最小数据单位
  • 伪共享是指两个不同的 CPU 访问共享同一缓存行的不同变量。理论上它们可以并行操作,但最终它们都在竞争更新同一个缓存条目。这会导致并发程序的性能大幅下降

1.14.4. 复杂类型的内存表示

  • 元组:其内存表示类似于结构体;字段类型和元组元素类型按相同顺序排列
  • 数组:所包含类型的连续序列,元素之间没有填充
  • 联合:每个字段的布局选择是独立的;对齐方式是所有字段中的最大值
  • 枚举:类似于联合,但有一个额外的隐藏共享字段用于存储枚举变体的判别式。代码使用判别式的值来确定给定值包含哪个变体。判别式的大小取决于变体的数量

1.14.5. 动态大小类型与宽指针

Rust 中的大多数类型会自动实现 Sized trait。

Rust 需要知道其类型的一些细节,例如为特定类型的值分配多少空间。这使得动态大小类型的概念有些令人困惑。它们有时被称为 DST非大小类型,允许我们编写适用于运行时才知道大小的值的代码。

为了使用动态大小类型,Rust 提供了 Sized trait 来指示类型的大小是否在编译时已知。所有在编译时已知大小的内容都会自动实现此 trait。Rust 还会隐式地将 Sized trait 添加到每个泛型函数中。默认情况下,泛型函数仅适用于编译时已知大小的类型。 这个限制可以通过 ?Sized 放宽。?Sized 表示“T 可能实现 Sized,也可能不实现”,即 T 可能是也可能不是动态大小类型。此表示法不需要泛型类型必须在编译时具有已知大小这一默认条件。?Trait 语法仅适用于 Sized trait,而不适用于其他 trait。

当一个函数需要接受 DST(如 trait 对象或切片)作为参数时,我们应该怎么做?我们可以使用宽指针(也称为胖指针)。

1.14.6. 宽指针

通过将非 Sized 类型放在宽指针之后,我们弥合了 Sized 类型和非 Sized 类型之间的差距。

那么,宽指针到底是什么?宽指针是一个普通指针,附加了一个“字大小”字段。它为编译器提供了关于指针的额外信息,以便生成使用该指针的合理代码。

当你引用 DST 时,编译器会自动为你构造一个宽指针。例如,对于切片,额外的信息是切片的长度。

宽指针是 Sized 的,因为它们本质上是指针,其大小是固定的(是“大小已知”指针大小的两倍——一个字段存储指针本身,另一个字段存储用于“完成”类型的附加元数据)。

注意:Box<T>Arc<T> 都支持存储宽指针,因此它们都支持 ?Sized