为什么大小为::<;MyStruct>;()不等于其字段大小之和?

为什么大小为::<;MyStruct>;()不等于其字段大小之和?,struct,rust,memory-alignment,memory-layout,Struct,Rust,Memory Alignment,Memory Layout,我试图测量结构及其字段()的大小: 因此,结构的大小大于其字段大小之和(即5)。为什么会这样?差异是由于为了满足a类型的要求。特定类型的值不希望存在于任意地址,而只存在于可被类型对齐整除的地址。例如,以char为例:它的对齐方式是4,因此它只希望生活在可以被4整除的地址上,比如0x4、0x8或0x7ffd463761bc,而不希望生活在0x6或0x7FFD4637611BD这样的地址上 类型的对齐依赖于平台,但通常情况下,大小为1、2或4的类型也分别具有1、2和4的对齐。对齐1意味着该类型的值在

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

因此,结构的大小大于其字段大小之和(即
5
)。为什么会这样?

差异是由于为了满足a类型的要求。特定类型的值不希望存在于任意地址,而只存在于可被类型对齐整除的地址。例如,以
char
为例:它的对齐方式是
4
,因此它只希望生活在可以被4整除的地址上,比如
0x4
0x8
0x7ffd463761bc
,而不希望生活在
0x6
0x7FFD4637611BD
这样的地址上

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

那么现在你的结构呢?生锈了

复合结构的对齐方式等于其场对齐方式的最大值

这意味着
MyStruct
类型的对齐方式也是
4
。我们可以通过和检查:

Oops
obj.bar
现在位于
0x5
,尽管它的对齐方式是4!那太糟糕了

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

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]
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:  ...
0x4:[obj.foo]
0x5:填充(未使用)
0x6:填充(未使用)
0x7:填充(未使用)
0x8:[对象条的第一个字节]
0x9:[对象条的第二个字节]
0xA:[对象条的第三个字节]
0xB:[对象条的第四个字节]
由于这个原因,
MyStruct
的大小是8,因为编译器添加了3个填充字节。现在一切又好了

。。。除了浪费的空间?事实上,这是不幸的。解决方案是交换结构的字段。幸运的是,在C++中,结构的内存布局未指定,这与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]
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:  ...
看起来像是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:   ...
哎呀,现在数组的第二个元素的
条开始于
0x9
!因此,实际上,数组大小需要是其对齐的倍数。因此,我们的记忆是这样的:

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]
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)]
布局之外,还有其他可用选项

您可以使用
#[repr(packed)]
使您的表示更加紧凑:

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

MyStruct:5
obj:5
obj.foo:1
对象栏:4

这可能不如默认的Rust表示,许多CPU根本不支持它,尤其是较旧的CPU或智能手机上的CPU。至少在某些现代CPU上的某些用例很少或没有性能损失(但您也应该阅读本文的评论,因为它们包含许多反例)。

可能值得注意的是,尽管内存布局优化在理论上一直是可能的,它直到今年才真正实现(多亏了)。还值得注意的是,结构的大小始终是其对齐的倍数,因此按顺序排列的数组元素保持对齐。因此,即使将char放在第一位,将u8放在第二位,也仍然有填充。如果结构中有多个U8,则重新排列元素很有趣;编译器可以将它们组合在一起。