为什么 size_of::<MyStruct>() 不等于其字段大小的总和?

Why is size_of::<MyStruct>() not equal to the sum of the sizes of its fields?

我尝试测量结构及其字段的大小 (Playground):

use std::mem;

struct MyStruct {
    foo: u8,
    bar: char,
}

println!("MyStruct: {}", mem::size_of::<MyStruct>());

let obj = MyStruct { foo: 0, bar: '0' };
println!("obj:      {}", mem::size_of_val(&obj));
println!("obj.foo:  {}", mem::size_of_val(&obj.foo));
println!("obj.bar:  {}", mem::size_of_val(&obj.bar));

这个程序打印:

MyStruct: 8
obj:      8
obj.foo:  1
obj.bar:  4

因此结构的大小大于其字段大小的总和(即 5)。这是为什么?

不同之处在于 padding in order to satisfy a types alignment 要求。特定类型的值不想存在于任意地址,而只存在于可被类型的 alignment 整除的 地址。例如,以 char 为例:它的对齐方式为 4,因此它只想位于可被 4 整除的地址,例如 0x40x80x7ffd463761bc ,而不是像 0x60x7ffd463761bd.

这样的地址

类型的对齐取决于平台,但大小为 124 的类型通常具有 124 也分别。 1 的对齐意味着该类型的值在任何地址都感觉舒适(因为任何地址都可以被 1 整除)。

那么现在你的结构呢?在 Rust 中,

composite structures will have an alignment equal to the maximum of their fields' alignment.

这意味着您的 MyStruct 类型的对齐方式也是 4。我们可以用 mem::align_of() and mem::align_of_val():

检查
// prints "4"
println!("{}", mem::align_of::<MyStruct>());

现在假设您的结构的值位于 0x4(满足结构的直接对齐要求):

0x4:   [obj.foo]
0x5:   [obj.bar's first byte]
0x6:   [obj.bar's second byte]
0x7:   [obj.bar's third byte]
0x8:   [obj.bar's fourth byte]

哎呀obj.bar 现在位于 0x5,尽管它的对齐方式是 4!那很糟!

为了解决这个问题,Rust 编译器将所谓的 padding——未使用的字节——插入到结构中。在内存中它现在看起来像这样:

0x4:   [obj.foo]
0x5:   padding (unused)
0x6:   padding (unused)
0x7:   padding (unused)
0x8:   [obj.bar's first byte]
0x9:   [obj.bar's second byte]
0xA:   [obj.bar's third byte]
0xB:   [obj.bar's fourth byte]

因此,MyStruct的大小为8,因为编译器添加了3个填充字节。现在一切都好了!

... 除了浪费的 space?的确,这是不幸的。一种解决方案是交换结构的字段。幸运的是,Rust 中结构的内存布局是未指定的,这与 C 或 C++ 不同。特别是,允许​​ Rust 编译器更改字段的顺序。您不能假设 obj.foo 的地址低于 obj.bar

并且由于 Rust 1.18,此优化由编译器执行。


但即使使用更新或等于 1.18 的 Rust 编译器,您的结构仍然是 8 个字节的大小。为什么?

内存布局还有一条规则:结构的大小必须始终是其对齐方式的倍数。这对于能够在数组中密集布局这些结构很有用。假设编译器将重新排序我们的结构字段并且内存布局如下所示:

0x4:   [obj.bar's first byte]
0x5:   [obj.bar's second byte]
0x6:   [obj.bar's third byte]
0x7:   [obj.bar's fourth byte]
0x8:   [obj.foo]

看起来像 5 个字节,对吧?没有!想象一下有一个数组 [MyStruct]。在数组中,所有元素在内存中彼此相邻:

0x4:   [[0].bar's first byte]
0x5:   [[0].bar's second byte]
0x6:   [[0].bar's third byte]
0x7:   [[0].bar's fourth byte]
0x8:   [[0].foo]
0x9:   [[1].bar's first byte]
0xA:   [[1].bar's second byte]
0xB:   [[1].bar's third byte]
0xC:   [[1].bar's fourth byte]
0xD:   [[1].foo]
0xE:   ...

糟糕,现在数组的第二个元素 bar 开始于 0x9!所以实际上,数组大小需要是其对齐方式的倍数。因此,我们的记忆看起来像这样:

0x4:   [[0].bar's first byte]
0x5:   [[0].bar's second byte]
0x6:   [[0].bar's third byte]
0x7:   [[0].bar's fourth byte]
0x8:   [[0].foo]
0x9:   [[0]'s padding byte]
0xA:   [[0]'s padding byte]
0xB:   [[0]'s padding byte]
0xC:   [[1].bar's first byte]
0xD:   [[1].bar's second byte]
0xE:   [[1].bar's third byte]
0xF:   [[1].bar's fourth byte]
0x10:  [[1].foo]
0x11:  [[1]'s padding byte]
0x12:  [[1]'s padding byte]
0x13:  [[1]'s padding byte]
0x14:  ...

相关:

除了默认的 #[repr(Rust)] 布局外,还有其他可用选项,as explained in the Rustonomicon

您可以使用 #[repr(packed)]:

使您的表示更加紧凑
#[repr(packed)]
struct MyStruct {
    foo: u8,
    bar: char,
}

这会将所有字段对齐到最接近的字节,而不管它们的首选对齐方式如何。所以输出将是:

MyStruct: 5
obj:      5
obj.foo:  1
obj.bar:  4

这可能比默认的 Rust 表示性能低,而且许多 CPU 根本不支持它,尤其是较旧的 CPU 或智能手机上的 CPU。 evidence 至少 some 用例在至少 some 现代 CPU 上几乎没有或没有性能损失(但你还应该阅读文章的评论,因为它们包含很多反例)。