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

时间:2017-04-28 09:37:05

标签: struct rust memory-alignment memory-layout

我尝试测量结构及其字段(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)。那是为什么?

2 个答案:

答案 0 :(得分:5)

差异是由padding引起的,以满足类型alignment的要求。特定类型的值不希望生活在任意地址,但仅限于可被类型整除的地址。 对准 即可。例如,取char:它的对齐方为4,因此它只想生活在可被4整除的地址,例如0x40x80x7ffd463761bc ,而不是0x60x7ffd463761bd等地址。

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

那么你的结构呢?在Rust,

  

复合结构的对齐等于其字段的最大值。对准。

这意味着您的MyStruct类型的对齐方式也是4。我们可以使用mem::align_of()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 - 未使用的字节 - 插入到struct中。在内存中,它现在看起来像这样:

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个填充字节。现在一切都很好!

......除了浪费的空间?实际上,这是不幸的。解决方案是交换struct的字段。幸运的是,为了这个目的,Rust中的结构的内存布局是未指定的,与C或C ++不同。特别是,允许​​Rust编译器更改字段的顺序。您不能认为obj.foo的地址低于obj.bar

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

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

内存布局还有另一个规则:结构的大小必须始终是其对齐的倍数。这对于能够在阵列中密集布局这些结构非常有用。假设编译器将重新排序我们的struct字段,内存布局如下所示:

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:   ...

哎呀,现在阵列的第二个元素bar0x9开始!实际上,数组大小需要是其对齐的倍数。因此,我们的记忆如下:

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:  ...

相关

答案 1 :(得分:3)

除了默认的packet MyPacket { string msg; int srcAddress; int destAddress; }; 布局外,还有其他可用选项,as explained in the Rustonomicon

您可以使用#[repr(Rust)]

使您的展示更紧密
#[repr(packed)]

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

#[repr(packed)]
struct MyStruct {
    foo: u8,
    bar: char,
}

这可能不如默认的Rust表示高,并且许多CPU根本不支持它,特别是旧CPU或智能手机上的CPU。 evidence至少某些现代CPU上的某些用例几乎没有性能损失(但您还应该阅读文章&# 39;评论,因为它们包含许多反例)。