【问题标题】:Why is size_of::<MyStruct>() not equal to the sum of the sizes of its fields?为什么 size_of::<MyStruct>() 不等于其字段大小的总和?
【发布时间】:2017-09-26 09:05:41
【问题描述】:

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

【问题讨论】:

    标签: struct rust memory-alignment memory-layout


    【解决方案1】:

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

    类型的对齐方式取决于平台,但通常大小为124 的类型也分别具有124 的对齐方式。 1 的对齐意味着该类型的值在任何地址都感觉舒适(因为任何地址都可以被 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(未使用的字节)插入到结构中。在内存中它现在看起来像这样:

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

    ...除了浪费的空间?的确,这是不幸的。一个解决方案是交换结构的字段。幸运的是,与 C 或 C++ 不同,Rust 中结构的内存布局是未指定的。特别是,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:  ...
    

    相关

    【讨论】:

    • 可能值得注意的是,虽然内存布局优化在理论上一直是可行的,但直到今年才真正实现(感谢some really awesome work by Austin Hicks)。
    • 另外值得注意的是,结构的大小将始终是其对齐方式的倍数,因此按顺序排列的数组元素保持对齐。因此,即使您将 char 放在第一位,u8 放在第二位,您仍然会有填充。如果结构中有多个 u8,重新排列元素会很有趣;编译器可以将它们组合在一起。
    【解决方案2】:

    除了默认的#[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 至少 一些 现代 CPU 上的至少 一些 用例几乎没有或没有性能损失(但您也应该阅读文章的 cmets,如它们包含很多反例)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-01-31
      • 1970-01-01
      • 2017-07-15
      • 2014-04-23
      • 2011-03-22
      • 2016-01-19
      • 2016-03-25
      相关资源
      最近更新 更多