为什么元组或结构的大小不是成员的总和?

Why is the size of a tuple or struct not the sum of the members?

assert_eq!(12, mem::size_of::<(i32, f64)>()); // failed
assert_eq!(16, mem::size_of::<(i32, f64)>()); // succeed
assert_eq!(16, mem::size_of::<(i32, f64, i32)>()); // succeed

为什么不是12(4+8)? Rust 对元组有特殊处理吗?

Why is it not 12 (4 + 8)? Does Rust have special treatment for tuples?

没有。常规结构可以(并且确实)具有相同的“问题”。

答案是padding:在64位系统上,一个f64应该对齐8个字节(即它的起始地址应该是8的倍数)。结构通常具有其最具约束力(最大对齐)成员的对齐方式,因此元组的对齐方式为 8.

这意味着您的元组必须以 8 的倍数的地址开始,因此 i32 以 8 的倍数开始,以 4 的倍数结束(因为它是 4 个字节) ,并且编译器添加了 4 个字节的填充,因此 f64 正确对齐:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[ i32 ] padding [     f64     ]

“等等”,你喊道,“如果我反转元组的字段,大小不会改变!”。

没错:上面的架构不准确,因为默认情况下 rustc 会将您的字段重新排序为紧凑结构,因此 确实 会这样做:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[     f64     ] [ i32 ] padding 

这就是为什么您的第三次尝试是 16 个字节的原因:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[     f64     ] [ i32 ] [ i32 ]

而不是 24:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[  32 ] padding [     f64     ] [  32 ] padding 

你说“别急”,你的眼睛很敏锐,“我可以看到 f64 的对齐方式,但为什么最后会有填充?那里没有 f64!”

好吧,这样计算机就可以更轻松地处理序列:具有给定对齐方式的结构的大小也应该是其对齐方式的倍数,这样当你有其中的倍数:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[     f64     ] [ i32 ] padding [     f64     ] [ i32 ] padding 

它们正确对齐如何放置下一个的计算很简单(只是抵消了结构的大小),它也避免了把这个信息无处不在。基本上,数组 / vec 本身永远不会被填充,而是填充在它存储的结构中。这允许 packing 成为结构 属性 而不会感染数组。


使用 repr(C) 属性,您可以告诉 Rust 完全按照您给出的顺序放置您的结构(这不是元组 FWIW 的选项)。

这是安全的,虽然它不是通常有用,但在一些边缘情况下它很重要,我知道的(可能还有其他)是:

  • 与外部 (FFI) 代码接口,它需要一个非常具体的布局,这实际上是标志名称的来源(它使 Rust 表现得像 C)。
  • 在高性能代码中避免 false sharing

您还可以告诉 rustc 不要使用 repr(packed) 填充结构

风险更大,它通常会降低性能(大多数 CPU 与未对齐的数据相当交叉)并且 可能会使程序崩溃或 return 完全错误的数据 在某些架构上。这高度依赖于 CPU 体系结构和系统 (OS) 运行:根据 the kernel's Unaligned Memory Accesses document

  1. 一些架构能够执行未对齐的内存访问 透明,但通常会有显着的性能成本。
  2. 一些架构在未对齐访问时引发处理器异常 发生。异常处理程序能够纠正未对齐的访问, 以显着的性能成本为代价。
  3. 一些架构在未对齐访问时引发处理器异常 发生,但异常不包含足够的信息 未对齐的访问被更正。
  4. 有些架构不支持未对齐的内存访问,但可以 静静地对请求的内存执行不同的内存访问, 导致难以检测的细微代码错误!

所以“Class 1”架构将执行正确的访问,可能会以性能成本为代价。

"Class 2" 架构将以高性能成本执行正确的访问(CPU 需要调用 OS,未对齐的访问被转换为对齐访问 in software),假设 OS 处理这种情况(在这种情况下并不总是解析为 class 3 架构)。

"Class 3" 架构将在未对齐访问时终止程序(因为系统无法修复它。

"Class 4" 将对未对齐的访问执行无意义的操作,并且是迄今为止最糟糕的。

另一个常见的陷阱或未对齐访问是它们往往是非原子的(因为它们需要扩展成一系列对齐的内存操作和操作),因此您可能会“撕裂”读取或写入即使对于其他原子访问。

虽然 @Masklinn 已经 这是由于对齐,这里是 Rust 参考:

Size and Alignment

All values have an alignment and size.

The alignment of a value specifies what addresses are valid to store the value at. A value of alignment n must only be stored at an address that is a multiple of n. For example, a value with an alignment of 2 must be stored at an even address, while a value with an alignment of 1 can be stored at any address. Alignment is measured in bytes, and must be at least 1, and always a power of 2. The alignment of a value can be checked with the align_of_val function.

The size of a value is the offset in bytes between successive elements in an array with that item type including alignment padding. The size of a value is always a multiple of its alignment. The size of a value can be checked with the size_of_val function.

[...]

The Rust Reference - Type Layout - Size and Alignment

(强调我的)