完整标题: [高级 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。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.