为什么 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 整除的地址,例如 0x4
、0x8
或 0x7ffd463761bc
,而不是像 0x6
或 0x7ffd463761bd
.
这样的地址
类型的对齐取决于平台,但大小为 1
、2
或 4
的类型通常具有 1
、2
和 4
也分别。 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: ...
相关:
- Chapter about memory layout in the Rustonomicon
- Similar question on the C++ tag
除了默认的 #[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 上几乎没有或没有性能损失(但你还应该阅读文章的评论,因为它们包含很多反例)。
我尝试测量结构及其字段的大小 (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 整除的地址,例如 0x4
、0x8
或 0x7ffd463761bc
,而不是像 0x6
或 0x7ffd463761bd
.
类型的对齐取决于平台,但大小为 1
、2
或 4
的类型通常具有 1
、2
和 4
也分别。 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: ...
相关:
- Chapter about memory layout in the Rustonomicon
- Similar question on the C++ tag
除了默认的 #[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 上几乎没有或没有性能损失(但你还应该阅读文章的评论,因为它们包含很多反例)。